improve media upload error messages (#2602)

This commit is contained in:
Konrad Pozniak 2022-06-30 20:51:05 +02:00 committed by GitHub
parent ffb8ba2276
commit 62c4cfde89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 61 additions and 32 deletions

View file

@ -161,7 +161,7 @@ class ComposeActivity :
val uriNew = result.uriContent val uriNew = result.uriContent
if (result.isSuccessful && uriNew != null) { if (result.isSuccessful && uriNew != null) {
viewModel.cropImageItemOld?.let { itemOld -> viewModel.cropImageItemOld?.let { itemOld ->
val size = getMediaSize(getApplicationContext().getContentResolver(), uriNew) val size = getMediaSize(contentResolver, uriNew)
lifecycleScope.launch { lifecycleScope.launch {
viewModel.addMediaToQueue( viewModel.addMediaToQueue(
@ -407,8 +407,13 @@ class ComposeActivity :
enableButton(binding.composeAddMediaButton, active, active) enableButton(binding.composeAddMediaButton, active, active)
enablePollButton(media.isNullOrEmpty()) enablePollButton(media.isNullOrEmpty())
}.subscribe() }.subscribe()
viewModel.uploadError.observe { viewModel.uploadError.observe { throwable ->
displayTransientError(R.string.error_media_upload_sending) Log.w(TAG, "media upload failed", throwable)
if (throwable is UploadServerError) {
displayTransientError(throwable.errorMessage)
} else {
displayTransientError(R.string.error_media_upload_sending)
}
} }
viewModel.setupComplete.observe { viewModel.setupComplete.observe {
// Focus may have changed during view model setup, ensure initial focus is on the edit field // Focus may have changed during view model setup, ensure initial focus is on the edit field
@ -553,19 +558,23 @@ class ComposeActivity :
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
private fun displayTransientError(@StringRes stringId: Int) { private fun displayTransientError(errorMessage: String) {
val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG) val bar = Snackbar.make(binding.activityCompose, errorMessage, Snackbar.LENGTH_LONG)
// necessary so snackbar is shown over everything // necessary so snackbar is shown over everything
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
bar.setAnchorView(R.id.composeBottomBar)
bar.show() bar.show()
} }
private fun displayTransientError(@StringRes stringId: Int) {
displayTransientError(getString(stringId))
}
private fun toggleHideMedia() { private fun toggleHideMedia() {
this.viewModel.toggleMarkSensitive() this.viewModel.toggleMarkSensitive()
} }
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
if (viewModel.media.value.isNullOrEmpty()) { if (viewModel.media.value.isEmpty()) {
binding.composeHideMediaButton.hide() binding.composeHideMediaButton.hide()
} else { } else {
binding.composeHideMediaButton.show() binding.composeHideMediaButton.show()
@ -904,11 +913,10 @@ class ComposeActivity :
// Currently the only supported lossless format is png. // Currently the only supported lossless format is png.
val mimeType: String? = contentResolver.getType(item.uri) val mimeType: String? = contentResolver.getType(item.uri)
val isPng: Boolean = mimeType != null && mimeType.endsWith("/png") val isPng: Boolean = mimeType != null && mimeType.endsWith("/png")
val context = getApplicationContext() val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
val tempFile = createNewImageFile(context, if (isPng) ".png" else ".jpg")
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml // "Authority" must be the same as the android:authorities string in AndroidManifest.xml
val uriNew = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile) val uriNew = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile)
viewModel.cropImageItemOld = item viewModel.cropImageItemOld = item

View file

@ -219,7 +219,7 @@ class ComposeViewModel @Inject constructor(
val contentWarningChanged = showContentWarning.value!! && val contentWarningChanged = showContentWarning.value!! &&
!contentWarning.isNullOrEmpty() && !contentWarning.isNullOrEmpty() &&
!startingContentWarning.startsWith(contentWarning.toString()) !startingContentWarning.startsWith(contentWarning.toString())
val mediaChanged = !media.value.isNullOrEmpty() val mediaChanged = media.value.isNotEmpty()
val pollChanged = poll.value != null val pollChanged = poll.value != null
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged

View file

@ -23,7 +23,7 @@ import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import at.connyduck.calladapter.networkresult.getOrThrow import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
@ -32,6 +32,7 @@ import com.keylesspalace.tusky.network.ProgressRequestBody
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
import com.keylesspalace.tusky.util.getImageSquarePixels import com.keylesspalace.tusky.util.getImageSquarePixels
import com.keylesspalace.tusky.util.getMediaSize import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.getServerErrorMessage
import com.keylesspalace.tusky.util.randomAlphanumericString import com.keylesspalace.tusky.util.randomAlphanumericString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -73,6 +74,7 @@ class AudioSizeException : Exception()
class VideoSizeException : Exception() class VideoSizeException : Exception()
class MediaTypeException : Exception() class MediaTypeException : Exception()
class CouldNotOpenFileException : Exception() class CouldNotOpenFileException : Exception()
class UploadServerError(val errorMessage: String) : Exception()
class MediaUploader @Inject constructor( class MediaUploader @Inject constructor(
private val context: Context, private val context: Context,
@ -223,8 +225,16 @@ class MediaUploader @Inject constructor(
null null
} }
val result = mediaUploadApi.uploadMedia(body, description).getOrThrow() mediaUploadApi.uploadMedia(body, description).fold({ result ->
send(UploadEvent.FinishedEvent(result.id)) send(UploadEvent.FinishedEvent(result.id))
}, { throwable ->
val errorMessage = throwable.getServerErrorMessage()
if (errorMessage == null) {
throw throwable
} else {
throw UploadServerError(errorMessage)
}
})
awaitClose() awaitClose()
} }
} }
@ -241,7 +251,7 @@ class MediaUploader @Inject constructor(
} }
private companion object { private companion object {
private const val TAG = "MediaUploaderImpl" private const val TAG = "MediaUploader"
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB

View file

@ -0,0 +1,26 @@
package com.keylesspalace.tusky.util
import org.json.JSONException
import org.json.JSONObject
import retrofit2.HttpException
/**
* checks if this throwable indicates an error causes by a 4xx/5xx server response and
* tries to retrieve the error message the server sent
* @return the error message, or null if this is no server error or it had no error message
*/
fun Throwable.getServerErrorMessage(): String? {
if (this is HttpException) {
val errorResponse = response()?.errorBody()?.string()
return if (!errorResponse.isNullOrBlank()) {
try {
JSONObject(errorResponse).getString("error")
} catch (e: JSONException) {
null
}
} else {
null
}
}
return null
}

View file

@ -32,6 +32,7 @@ import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.getServerErrorMessage
import com.keylesspalace.tusky.util.randomAlphanumericString import com.keylesspalace.tusky.util.randomAlphanumericString
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -39,9 +40,6 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONException
import org.json.JSONObject
import retrofit2.HttpException
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -156,21 +154,7 @@ class EditProfileViewModel @Inject constructor(
eventHub.dispatch(ProfileEditedEvent(newProfileData)) eventHub.dispatch(ProfileEditedEvent(newProfileData))
}, },
{ throwable -> { throwable ->
if (throwable is HttpException) { saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage()))
val errorResponse = throwable.response()?.errorBody()?.string()
val errorMsg = if (!errorResponse.isNullOrBlank()) {
try {
JSONObject(errorResponse).optString("error", "")
} catch (e: JSONException) {
null
}
} else {
null
}
saveData.postValue(Error(errorMessage = errorMsg))
} else {
saveData.postValue(Error())
}
} }
) )
} }

View file

@ -239,6 +239,7 @@
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
<LinearLayout <LinearLayout
android:id="@+id/composeBottomBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_gravity="bottom"