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:
parent
bcf99e1e6e
commit
be96aa576e
4 changed files with 123 additions and 0 deletions
|
@ -41,6 +41,7 @@ import androidx.core.view.ViewCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.keylesspalace.tusky.BaseActivity;
|
import com.keylesspalace.tusky.BaseActivity;
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity;
|
import com.keylesspalace.tusky.BottomSheetActivity;
|
||||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
||||||
|
@ -290,6 +291,14 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
}
|
}
|
||||||
case R.id.pin: {
|
case R.id.pin: {
|
||||||
timelineCases.pin(status.getId(), !status.isPinned())
|
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)))
|
.to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||||
.subscribe();
|
.subscribe();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.keylesspalace.tusky.entity.DeletedStatus
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.getServerErrorMessage
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.addTo
|
import io.reactivex.rxjava3.kotlin.addTo
|
||||||
|
@ -130,6 +131,10 @@ class TimelineCases @Inject constructor(
|
||||||
fun pin(statusId: String, pin: Boolean): Single<Status> {
|
fun pin(statusId: String, pin: Boolean): Single<Status> {
|
||||||
// Replace with extension method if we use RxKotlin
|
// Replace with extension method if we use RxKotlin
|
||||||
return (if (pin) mastodonApi.pinStatus(statusId) else mastodonApi.unpinStatus(statusId))
|
return (if (pin) mastodonApi.pinStatus(statusId) else mastodonApi.unpinStatus(statusId))
|
||||||
|
.doOnError { e ->
|
||||||
|
Log.w("Failed to change pin state", e)
|
||||||
|
}
|
||||||
|
.onErrorResumeNext(::convertError)
|
||||||
.doAfterSuccess {
|
.doAfterSuccess {
|
||||||
eventHub.dispatch(PinEvent(statusId, pin))
|
eventHub.dispatch(PinEvent(statusId, pin))
|
||||||
}
|
}
|
||||||
|
@ -144,4 +149,10 @@ class TimelineCases @Inject constructor(
|
||||||
eventHub.dispatch(PollVoteEvent(statusId, it))
|
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)
|
||||||
|
|
|
@ -467,6 +467,8 @@
|
||||||
|
|
||||||
<string name="unpin_action">Unpin</string>
|
<string name="unpin_action">Unpin</string>
|
||||||
<string name="pin_action">Pin</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">
|
<plurals name="favs">
|
||||||
<item quantity="one"><b>%1$s</b> Favorite</item>
|
<item quantity="one"><b>%1$s</b> Favorite</item>
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue