- 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:
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user