Show toast if pin fails (#2755)

* Show toast if pin fails

Fixes #2229

* Swtich to snackbar

* Show generic error message if no server error is available

* Fix pin error logging
This commit is contained in:
Eva Tatarka 2022-11-09 13:30:50 -05:00 committed by GitHub
parent bcf99e1e6e
commit be96aa576e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 123 additions and 0 deletions

View File

@ -41,6 +41,7 @@ import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import com.google.android.material.snackbar.Snackbar;
import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.BottomSheetActivity;
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
@ -290,6 +291,14 @@ public abstract class SFragment extends Fragment implements Injectable {
}
case R.id.pin: {
timelineCases.pin(status.getId(), !status.isPinned())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(e -> {
String message = e.getMessage();
if (message == null) {
message = getString(status.isPinned() ? R.string.failed_to_unpin : R.string.failed_to_pin);
}
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
})
.to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe();
return true;

View File

@ -30,6 +30,7 @@ import com.keylesspalace.tusky.entity.DeletedStatus
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.getServerErrorMessage
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.addTo
@ -130,6 +131,10 @@ class TimelineCases @Inject constructor(
fun pin(statusId: String, pin: Boolean): Single<Status> {
// Replace with extension method if we use RxKotlin
return (if (pin) mastodonApi.pinStatus(statusId) else mastodonApi.unpinStatus(statusId))
.doOnError { e ->
Log.w("Failed to change pin state", e)
}
.onErrorResumeNext(::convertError)
.doAfterSuccess {
eventHub.dispatch(PinEvent(statusId, pin))
}
@ -144,4 +149,10 @@ class TimelineCases @Inject constructor(
eventHub.dispatch(PollVoteEvent(statusId, it))
}
}
private fun <T : Any> convertError(e: Throwable): Single<T> {
return Single.error(TimelineError(e.getServerErrorMessage()))
}
}
class TimelineError(message: String?) : RuntimeException(message)

View File

@ -467,6 +467,8 @@
<string name="unpin_action">Unpin</string>
<string name="pin_action">Pin</string>
<string name="failed_to_pin">Failed to Pin</string>
<string name="failed_to_unpin">Failed to Unpin</string>
<plurals name="favs">
<item quantity="one">&lt;b>%1$s&lt;/b> Favorite</item>

View File

@ -0,0 +1,101 @@
package com.keylesspalace.tusky.usecase
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PinEvent
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import io.reactivex.rxjava3.core.Single
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.robolectric.annotation.Config
import retrofit2.HttpException
import retrofit2.Response
import java.util.Date
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class TimelineCasesTest {
private lateinit var api: MastodonApi
private lateinit var eventHub: EventHub
private lateinit var timelineCases: TimelineCases
private val statusId = "1234"
@Before
fun setup() {
api = mock()
eventHub = EventHub()
timelineCases = TimelineCases(api, eventHub)
}
@Test
fun `pin success emits PinEvent`() {
api.stub {
onBlocking { pinStatus(statusId) } doReturn Single.just(mockStatus(pinned = true))
}
val events = eventHub.events.test()
timelineCases.pin(statusId, true)
.test()
.assertComplete()
events.assertValue(PinEvent(statusId, true))
}
@Test
fun `pin failure with server error throws TimelineError with server message`() {
api.stub {
onBlocking { pinStatus(statusId) } doReturn Single.error(
HttpException(
Response.error<Status>(
422,
"{\"error\":\"Validation Failed: You have already pinned the maximum number of toots\"}".toResponseBody()
)
)
)
}
timelineCases.pin(statusId, true)
.test()
.assertError { it.message == "Validation Failed: You have already pinned the maximum number of toots" }
}
private fun mockStatus(pinned: Boolean = false): Status {
return Status(
id = "123",
url = "https://mastodon.social/@Tusky/100571663297225812",
account = mock(),
inReplyToId = null,
inReplyToAccountId = null,
reblog = null,
content = "",
createdAt = Date(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
reblogged = false,
favourited = false,
bookmarked = false,
sensitive = false,
spoilerText = "",
visibility = Status.Visibility.PUBLIC,
attachments = arrayListOf(),
mentions = listOf(),
tags = listOf(),
application = null,
pinned = pinned,
muted = false,
poll = null,
card = null,
language = null,
)
}
}