From be96aa576ec60a1a81a588fa9b501457f36913bb Mon Sep 17 00:00:00 2001 From: Eva Tatarka Date: Wed, 9 Nov 2022 13:30:50 -0500 Subject: [PATCH] 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 --- .../tusky/fragment/SFragment.java | 9 ++ .../tusky/usecase/TimelineCases.kt | 11 ++ app/src/main/res/values/strings.xml | 2 + .../tusky/usecase/TimelineCasesTest.kt | 101 ++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index c859cb2a8..9ef26dacb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -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; diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt index 8f1144340..570e1e392 100644 --- a/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt @@ -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 { // 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 convertError(e: Throwable): Single { + return Single.error(TimelineError(e.getServerErrorMessage())) + } } + +class TimelineError(message: String?) : RuntimeException(message) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bea71bda6..ef890b1e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -467,6 +467,8 @@ Unpin Pin + Failed to Pin + Failed to Unpin <b>%1$s</b> Favorite diff --git a/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt b/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt new file mode 100644 index 000000000..f217a18b9 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt @@ -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( + 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, + ) + } +}