From 9026c877acc89cf24cdd02a7cc4bcdd26b67d269 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 17 Dec 2023 17:26:17 -0500 Subject: [PATCH] - 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 --- .../amethyst/service/FileHeader.kt | 10 +-- .../amethyst/ui/actions/NewMediaModel.kt | 61 +++++++++++-------- .../amethyst/ui/actions/NewMediaView.kt | 19 +++--- .../amethyst/ui/actions/NewPostView.kt | 6 +- .../amethyst/ui/actions/NewPostViewModel.kt | 17 ++++-- app/src/main/res/values/strings.xml | 8 +++ 6 files changed, 71 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt index 54f60e1d9..7f42f671b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt @@ -28,7 +28,7 @@ class FileHeader( mimeType: String?, dimPrecomputed: String?, onReady: (FileHeader) -> Unit, - onError: () -> Unit + onError: (String?) -> Unit ) { try { val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl) @@ -36,11 +36,11 @@ class FileHeader( if (imageData != null) { prepare(imageData, mimeType, dimPrecomputed, onReady, onError) } else { - onError() + onError(null) } } catch (e: Exception) { Log.e("ImageDownload", "Couldn't download image from server: ${e.message}") - onError() + onError(e.message) } } @@ -49,7 +49,7 @@ class FileHeader( mimeType: String?, dimPrecomputed: String?, onReady: (FileHeader) -> Unit, - onError: () -> Unit + onError: (String?) -> Unit ) { try { val hash = CryptoUtils.sha256(data).toHexKey() @@ -123,7 +123,7 @@ class FileHeader( onReady(FileHeader(mimeType, hash, size, dim, blurHash)) } catch (e: Exception) { Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}") - onError() + onError(e.message) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index bccfa9f5c..5aa7e43cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.FileHeader 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.ui.components.MediaCompressor import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch data class ServerOption( @@ -30,7 +29,6 @@ open class NewMediaModel : ViewModel() { var account: Account? = null var isUploadingImage by mutableStateOf(false) - val imageUploadingError = MutableSharedFlow(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) var mediaType by mutableStateOf(null) var selectedServer by mutableStateOf(null) @@ -44,12 +42,14 @@ open class NewMediaModel : ViewModel() { var uploadingDescription = mutableStateOf(null) 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.galleryUri = uri this.mediaType = contentType this.selectedServer = ServerOption(defaultServer(), false) + this.onError = onError } fun upload(context: Context, relayList: List? = null) { @@ -73,11 +73,11 @@ open class NewMediaModel : ViewModel() { uploadingPercentage.value = 0.2f uploadingDescription.value = "Loading" contentResolver.openInputStream(fileUri)?.use { - createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList) + createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList, context) } ?: run { viewModelScope.launch { - imageUploadingError.emit("Failed to upload the image / video") + onError(context.getString(R.string.could_not_open_the_compressed_file)) isUploadingImage = false uploadingPercentage.value = 0.00f uploadingDescription.value = null @@ -106,15 +106,14 @@ open class NewMediaModel : ViewModel() { localContentType = contentType, alt = alt, sensitiveContent = sensitiveContent, - relayList = relayList + relayList = relayList, + context ) } catch (e: Exception) { isUploadingImage = false uploadingPercentage.value = 0.00f uploadingDescription.value = null - viewModelScope.launch { - imageUploadingError.emit("Failed to upload: ${e.message}") - } + onError(context.getString(R.string.failed_to_upload_media, e.message)) } } } @@ -123,9 +122,7 @@ open class NewMediaModel : ViewModel() { isUploadingImage = false uploadingPercentage.value = 0.00f uploadingDescription.value = null - viewModelScope.launch { - imageUploadingError.emit("Failed to upload the image / video") - } + onError(context.getString(R.string.error_when_compressing_media, it)) } ) } @@ -151,7 +148,8 @@ open class NewMediaModel : ViewModel() { localContentType: String?, alt: String, sensitiveContent: Boolean, - relayList: List? = null + relayList: List? = null, + context: Context ) { uploadingPercentage.value = 0.40f uploadingDescription.value = "Server Processing" @@ -169,9 +167,7 @@ open class NewMediaModel : ViewModel() { uploadingPercentage.value = 0.00f uploadingDescription.value = null isUploadingImage = false - viewModelScope.launch { - imageUploadingError.emit("Failed to upload the image / video") - } + onError(context.getString(R.string.server_did_not_provide_a_url_after_uploading)) return } @@ -203,9 +199,7 @@ open class NewMediaModel : ViewModel() { uploadingPercentage.value = 0.00f uploadingDescription.value = null isUploadingImage = false - viewModelScope.launch { - imageUploadingError.emit("Failed to upload the image / video") - } + onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it)) } ) } else { @@ -214,13 +208,28 @@ open class NewMediaModel : ViewModel() { uploadingPercentage.value = 0.00f uploadingDescription.value = null isUploadingImage = false - viewModelScope.launch { - imageUploadingError.emit("Failed to upload the image / video") - } + onError(context.getString(R.string.could_not_download_from_the_server)) } } - fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List? = null) { + fun createNIP95Record( + bytes: ByteArray, + mimeType: String?, + alt: String, + sensitiveContent: Boolean, + relayList: List? = 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 uploadingDescription.value = "Hashing" @@ -248,9 +257,7 @@ open class NewMediaModel : ViewModel() { uploadingPercentage.value = 0.00f isUploadingImage = false cancel() - viewModelScope.launch { - imageUploadingError.emit("Failed to upload the image / video") - } + onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it)) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 93d3e4347..cea93ab50 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -5,7 +5,6 @@ import android.net.Uri import android.os.Build import android.util.Log import android.util.Size -import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -55,7 +54,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Composable 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) { val mediaType = resolver.getType(uri) ?: "" - postViewModel.load(account, uri, mediaType) - - launch(Dispatchers.IO) { - postViewModel.imageUploadingError.collect { error -> - withContext(Dispatchers.Main) { - Toast.makeText(context, error, Toast.LENGTH_SHORT).show() - } - } + postViewModel.load(account, uri, mediaType) { + accountViewModel.toast(context.getString(R.string.failed_to_upload_media_no_details), it) } } @@ -146,7 +138,11 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac onPost = { onClose() postViewModel.upload(context, relayList) - postViewModel.selectedServer?.let { account.changeDefaultFileServer(it.server) } + postViewModel.selectedServer?.let { + if (!it.isNip95) { + account.changeDefaultFileServer(it.server) + } + } }, isActive = postViewModel.canPost() ) @@ -211,7 +207,6 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo try { bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null) } 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) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 5892fec01..4a0f3d71c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -405,8 +405,10 @@ fun NewPostView( url, accountViewModel.account.defaultFileServer, onAdd = { alt, server, sensitiveContent -> - postViewModel.upload(url, alt, sensitiveContent, false, server, context, relayList) - accountViewModel.account.changeDefaultFileServer(server.server) + postViewModel.upload(url, alt, sensitiveContent, false, server, context) + if (!server.isNip95) { + accountViewModel.account.changeDefaultFileServer(server.server) + } }, onCancel = { postViewModel.contentToAddUrl = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 7bdf9a86f..bc0da3b51 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.ZapSplitSetup import com.vitorpamplona.quartz.events.findURLs import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -206,7 +207,7 @@ open class NewPostViewModel() : ViewModel() { val dmUsers = toUsersTagger.mentions val zapReceiver = if (wantsForwardZapTo) { - forwardZapTo?.items?.map { + forwardZapTo.items.map { ZapSplitSetup( lnAddressOrPubKeyHex = it.key.pubkeyHex, relay = it.key.relaysBeingUsed.keys.firstOrNull(), @@ -361,8 +362,7 @@ open class NewPostViewModel() : ViewModel() { sensitiveContent: Boolean, isPrivate: Boolean = false, server: ServerOption, - context: Context, - relayList: List? = null + context: Context ) { isUploadingImage = true contentToAddUrl = null @@ -569,7 +569,7 @@ open class NewPostViewModel() : ViewModel() { TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length) ) } else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) { - forwardZapTo?.addItem(item) + forwardZapTo.addItem(item) forwardZapToEditting = TextFieldValue("") /* val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") @@ -740,6 +740,14 @@ open class NewPostViewModel() : ViewModel() { alt: String?, sensitiveContent: Boolean ) { + if (bytes.size > 80000) { + viewModelScope.launch { + imageUploadingError.emit("Media is too big for NIP-95") + isUploadingImage = false + } + return + } + viewModelScope.launch(Dispatchers.IO) { FileHeader.prepare( bytes, @@ -773,6 +781,7 @@ open class NewPostViewModel() : ViewModel() { contentToAddUrl = uri } + @OptIn(ExperimentalCoroutinesApi::class) fun startLocation(context: Context) { locUtil = LocationUtil(context) locUtil?.let { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98123fe62..4d859f890 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -684,4 +684,12 @@ Food Miscellaneous Other + + Failed to upload media + Could not open the compressed file + Error when compressing media: %1$s + Uploading error: %1$s + Server did not provide a URL after uploading + Could not download uploaded media from the server + Could not prepare local file to upload: %1$s