Show running image uploading. Show error message if the uploading is failed

This commit is contained in:
Oleg Koretsky
2023-02-13 16:24:49 +02:00
parent 108ad4dadc
commit bb76bbd313
4 changed files with 75 additions and 51 deletions

View File

@@ -15,12 +15,13 @@ object ImageUploader {
uri: Uri, uri: Uri,
contentResolver: ContentResolver, contentResolver: ContentResolver,
onSuccess: (String) -> Unit, onSuccess: (String) -> Unit,
) { onError: (Throwable) -> Unit,
) {
val contentType = contentResolver.getType(uri) val contentType = contentResolver.getType(uri)
val client = OkHttpClient.Builder().build() val client = OkHttpClient.Builder().build()
val body: RequestBody = MultipartBody.Builder() val requestBody: RequestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
.addFormDataPart( .addFormDataPart(
"image", "image",
@@ -30,9 +31,12 @@ object ImageUploader {
contentType?.toMediaType() contentType?.toMediaType()
override fun writeTo(sink: BufferedSink) { override fun writeTo(sink: BufferedSink) {
contentResolver.openInputStream(uri)!!.use { inputStream -> val imageInputStream = contentResolver.openInputStream(uri)
sink.writeAll(inputStream.source()) checkNotNull(imageInputStream) {
"Can't open the image input stream"
} }
imageInputStream.source().use(sink::writeAll)
} }
} }
) )
@@ -41,24 +45,31 @@ object ImageUploader {
val request: Request = Request.Builder() val request: Request = Request.Builder()
.url("https://api.imgur.com/3/image") .url("https://api.imgur.com/3/image")
.header("Authorization", "Client-ID e6aea87296f3f96") .header("Authorization", "Client-ID e6aea87296f3f96")
.post(body) .post(requestBody)
.build() .build()
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
response.use { try {
val body = response.body check(response.isSuccessful)
if (body != null) { response.body.use { body ->
val tree = jacksonObjectMapper().readTree(body.string()) val tree = jacksonObjectMapper().readTree(body.string())
val url = tree?.get("data")?.get("link")?.asText() val url = tree?.get("data")?.get("link")?.asText()
if (url != null) checkNotNull(url) {
onSuccess(url) "There must be an uploaded image URL in the response"
}
onSuccess(url)
} }
} catch (e: Exception) {
e.printStackTrace()
onError(e)
} }
} }
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
e.printStackTrace() e.printStackTrace()
onError(e)
} }
}) })
} }

View File

@@ -1,27 +1,13 @@
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button import androidx.compose.material.*
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -37,11 +23,9 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -49,12 +33,7 @@ import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.UrlPreview import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.components.imageExtension
import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
import com.vitorpamplona.amethyst.ui.components.videoExtension
import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery
import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.note.ReplyInformation
import com.vitorpamplona.amethyst.ui.screen.UserLine import com.vitorpamplona.amethyst.ui.screen.UserLine
@@ -78,6 +57,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
delay(100) delay(100)
focusRequester.requestFocus() focusRequester.requestFocus()
postViewModel.imageUploadingError.collect { error ->
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
} }
Dialog( Dialog(
@@ -106,7 +89,9 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account
onClose() onClose()
}) })
UploadFromGallery { UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
) {
postViewModel.upload(it, context) postViewModel.upload(it, context)
} }
@@ -115,7 +100,8 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account
postViewModel.sendPost() postViewModel.sendPost()
onClose() onClose()
}, },
postViewModel.message.text.isNotBlank() isActive = postViewModel.message.text.isNotBlank()
&& !postViewModel.isUploadingImage
) )
} }

View File

@@ -8,9 +8,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import nostr.postr.toNpub import nostr.postr.toNpub
class NewPostViewModel: ViewModel() { class NewPostViewModel: ViewModel() {
@@ -22,6 +25,8 @@ class NewPostViewModel: ViewModel() {
var message by mutableStateOf(TextFieldValue("")) var message by mutableStateOf(TextFieldValue(""))
var urlPreview by mutableStateOf<String?>(null) var urlPreview by mutableStateOf<String?>(null)
var isUploadingImage by mutableStateOf(false)
val imageUploadingError = MutableSharedFlow<String?>()
var userSuggestions by mutableStateOf<List<User>>(emptyList()) var userSuggestions by mutableStateOf<List<User>>(emptyList())
var userSuggestionAnchor: TextRange? = null var userSuggestionAnchor: TextRange? = null
@@ -96,21 +101,33 @@ class NewPostViewModel: ViewModel() {
message = TextFieldValue("") message = TextFieldValue("")
urlPreview = null urlPreview = null
isUploadingImage = false
} }
fun upload(it: Uri, context: Context) { fun upload(it: Uri, context: Context) {
isUploadingImage = true
ImageUploader.uploadImage( ImageUploader.uploadImage(
uri = it, uri = it,
contentResolver = context.contentResolver, contentResolver = context.contentResolver,
) { onSuccess = { imageUrl ->
message = TextFieldValue(message.text + "\n\n" + it) isUploadingImage = false
urlPreview = findUrlInMessage() message = TextFieldValue(message.text + "\n\n" + imageUrl)
} urlPreview = findUrlInMessage()
},
onError = {
isUploadingImage = false
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image")
}
}
)
} }
fun cancel() { fun cancel() {
message = TextFieldValue("") message = TextFieldValue("")
urlPreview = null urlPreview = null
isUploadingImage = false
} }
fun findUrlInMessage(): String? { fun findUrlInMessage(): String? {

View File

@@ -9,12 +9,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -24,7 +19,10 @@ import com.google.accompanist.permissions.rememberPermissionState
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun UploadFromGallery(onImageChosen: (Uri) -> Unit) { fun UploadFromGallery(
isUploading: Boolean,
onImageChosen: (Uri) -> Unit,
) {
val cameraPermissionState = val cameraPermissionState =
rememberPermissionState( rememberPermissionState(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -50,18 +48,30 @@ fun UploadFromGallery(onImageChosen: (Uri) -> Unit) {
modifier = Modifier modifier = Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.padding(4.dp), .padding(4.dp),
enabled = !isUploading,
onClick = { onClick = {
showGallerySelect = true showGallerySelect = true
} }
) { ) {
Text("Upload Image") if (!isUploading) {
Text("Upload Image")
} else {
Text("Uploading…")
}
} }
} }
} }
} else { } else {
Column { Column {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) { Button(
Text("Upload Image") onClick = { cameraPermissionState.launchPermissionRequest() },
enabled = !isUploading,
) {
if (!isUploading) {
Text("Upload Image")
} else {
Text("Uploading…")
}
} }
} }
} }