From bb76bbd3136c767fe82cca58a1fc78509f11d57f Mon Sep 17 00:00:00 2001 From: Oleg Koretsky Date: Mon, 13 Feb 2023 16:24:49 +0200 Subject: [PATCH] Show running image uploading. Show error message if the uploading is failed --- .../amethyst/ui/actions/ImageUploader.kt | 31 +++++++++----- .../amethyst/ui/actions/NewPostView.kt | 40 ++++++------------- .../amethyst/ui/actions/NewPostViewModel.kt | 25 ++++++++++-- .../amethyst/ui/actions/UploadFromGallery.kt | 30 +++++++++----- 4 files changed, 75 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index c32428264..ccb6b49cb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -15,12 +15,13 @@ object ImageUploader { uri: Uri, contentResolver: ContentResolver, onSuccess: (String) -> Unit, - ) { + onError: (Throwable) -> Unit, +) { val contentType = contentResolver.getType(uri) val client = OkHttpClient.Builder().build() - val body: RequestBody = MultipartBody.Builder() + val requestBody: RequestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart( "image", @@ -30,9 +31,12 @@ object ImageUploader { contentType?.toMediaType() override fun writeTo(sink: BufferedSink) { - contentResolver.openInputStream(uri)!!.use { inputStream -> - sink.writeAll(inputStream.source()) + val imageInputStream = contentResolver.openInputStream(uri) + 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() .url("https://api.imgur.com/3/image") .header("Authorization", "Client-ID e6aea87296f3f96") - .post(body) + .post(requestBody) .build() client.newCall(request).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { - response.use { - val body = response.body - if (body != null) { + try { + check(response.isSuccessful) + response.body.use { body -> val tree = jacksonObjectMapper().readTree(body.string()) val url = tree?.get("data")?.get("link")?.asText() - if (url != null) - onSuccess(url) + checkNotNull(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) { e.printStackTrace() + onError(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 e4ae2a48c..169bf826c 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 @@ -1,27 +1,13 @@ package com.vitorpamplona.amethyst.ui.actions +import android.widget.Toast import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -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.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Button -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.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel @@ -49,12 +33,7 @@ import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.components.UrlPreview -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.components.* import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.screen.UserLine @@ -78,6 +57,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account LaunchedEffect(Unit) { delay(100) focusRequester.requestFocus() + + postViewModel.imageUploadingError.collect { error -> + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } } Dialog( @@ -106,7 +89,9 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account onClose() }) - UploadFromGallery { + UploadFromGallery( + isUploading = postViewModel.isUploadingImage, + ) { postViewModel.upload(it, context) } @@ -115,7 +100,8 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account postViewModel.sendPost() onClose() }, - postViewModel.message.text.isNotBlank() + isActive = postViewModel.message.text.isNotBlank() + && !postViewModel.isUploadingImage ) } 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 c01078720..82762eab6 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 @@ -8,9 +8,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch import nostr.postr.toNpub class NewPostViewModel: ViewModel() { @@ -22,6 +25,8 @@ class NewPostViewModel: ViewModel() { var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) + var isUploadingImage by mutableStateOf(false) + val imageUploadingError = MutableSharedFlow() var userSuggestions by mutableStateOf>(emptyList()) var userSuggestionAnchor: TextRange? = null @@ -96,21 +101,33 @@ class NewPostViewModel: ViewModel() { message = TextFieldValue("") urlPreview = null + isUploadingImage = false } fun upload(it: Uri, context: Context) { + isUploadingImage = true + ImageUploader.uploadImage( uri = it, contentResolver = context.contentResolver, - ) { - message = TextFieldValue(message.text + "\n\n" + it) - urlPreview = findUrlInMessage() - } + onSuccess = { imageUrl -> + isUploadingImage = false + message = TextFieldValue(message.text + "\n\n" + imageUrl) + urlPreview = findUrlInMessage() + }, + onError = { + isUploadingImage = false + viewModelScope.launch { + imageUploadingError.emit("Failed to upload the image") + } + } + ) } fun cancel() { message = TextFieldValue("") urlPreview = null + isUploadingImage = false } fun findUrlInMessage(): String? { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt index 64d4bc0ea..2d4237f11 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt @@ -9,12 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -24,7 +19,10 @@ import com.google.accompanist.permissions.rememberPermissionState @OptIn(ExperimentalPermissionsApi::class) @Composable -fun UploadFromGallery(onImageChosen: (Uri) -> Unit) { +fun UploadFromGallery( + isUploading: Boolean, + onImageChosen: (Uri) -> Unit, +) { val cameraPermissionState = rememberPermissionState( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -50,18 +48,30 @@ fun UploadFromGallery(onImageChosen: (Uri) -> Unit) { modifier = Modifier .align(Alignment.TopCenter) .padding(4.dp), + enabled = !isUploading, onClick = { showGallerySelect = true } ) { - Text("Upload Image") + if (!isUploading) { + Text("Upload Image") + } else { + Text("Uploading…") + } } } } } else { Column { - Button(onClick = { cameraPermissionState.launchPermissionRequest() }) { - Text("Upload Image") + Button( + onClick = { cameraPermissionState.launchPermissionRequest() }, + enabled = !isUploading, + ) { + if (!isUploading) { + Text("Upload Image") + } else { + Text("Uploading…") + } } } }