Remove rxjava from API calls used by AccountViewModel::changeRelationship() (#3008)

* Remove rxjava from API calls used by AccountListFragment

* Remove rxjava from API calls used by AccountViewModel::changeRelationship()

The affected API functions are also called from

- ReportViewModel.kt
- SearchViewModel.kt
- AccountListFragment.kt
- SFragment.java
- TimelineCases.kt

so they have also been updated.

This change requires bridging from Java code to Kotlin `suspend` functions,
by creating wrappers for the `mute` and `block` functions that can be
called from Java and create a coroutine scope.

I've deliberately made this fairly ugly so that it sticks out and can be
removed later.

* Use "Throwable" type and name

* Delete 46.json

Not sure where this came from.

* Emit log messages with the correct tag

* Add another log tag, and lint

* Move viewModelScope.launch in to changeRelationshop()
This commit is contained in:
Nik Clayton 2022-12-28 19:06:31 +01:00 committed by GitHub
parent 68f20e03c4
commit a21f2fadf9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 142 deletions

View file

@ -2,6 +2,7 @@ package com.keylesspalace.tusky.components.account
import android.util.Log import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.BlockEvent
import com.keylesspalace.tusky.appstore.DomainMuteEvent import com.keylesspalace.tusky.appstore.DomainMuteEvent
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
@ -19,6 +20,7 @@ import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -181,7 +183,11 @@ class AccountViewModel @Inject constructor(
/** /**
* @param parameter showReblogs if RelationShipAction.FOLLOW, notifications if MUTE * @param parameter showReblogs if RelationShipAction.FOLLOW, notifications if MUTE
*/ */
private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null, duration: Int? = null) { private fun changeRelationship(
relationshipAction: RelationShipAction,
parameter: Boolean? = null,
duration: Int? = null
) = viewModelScope.launch {
val relation = relationshipData.value?.data val relation = relationshipData.value?.data
val account = accountData.value?.data val account = accountData.value?.data
val isMastodon = relationshipData.value?.data?.notifying != null val isMastodon = relationshipData.value?.data?.notifying != null
@ -216,12 +222,20 @@ class AccountViewModel @Inject constructor(
relationshipData.postValue(Loading(newRelation)) relationshipData.postValue(Loading(newRelation))
} }
when (relationshipAction) { try {
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs = parameter ?: true) val relationship = when (relationshipAction) {
RelationShipAction.FOLLOW -> mastodonApi.followAccount(
accountId,
showReblogs = parameter ?: true
)
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true, duration) RelationShipAction.MUTE -> mastodonApi.muteAccount(
accountId,
parameter ?: true,
duration
)
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
RelationShipAction.SUBSCRIBE -> { RelationShipAction.SUBSCRIBE -> {
if (isMastodon) if (isMastodon)
@ -233,8 +247,8 @@ class AccountViewModel @Inject constructor(
mastodonApi.followAccount(accountId, notify = false) mastodonApi.followAccount(accountId, notify = false)
else mastodonApi.unsubscribeAccount(accountId) else mastodonApi.unsubscribeAccount(accountId)
} }
}.subscribe( }
{ relationship ->
relationshipData.postValue(Success(relationship)) relationshipData.postValue(Success(relationship))
when (relationshipAction) { when (relationshipAction) {
@ -244,12 +258,9 @@ class AccountViewModel @Inject constructor(
else -> { else -> {
} }
} }
}, } catch (_: Throwable) {
{
relationshipData.postValue(Error(relation)) relationshipData.postValue(Error(relation))
} }
)
.autoDispose()
} }
fun noteChanged(newNote: String) { fun noteChanged(newNote: String) {

View file

@ -154,52 +154,46 @@ class ReportViewModel @Inject constructor(
fun toggleMute() { fun toggleMute() {
val alreadyMuted = muteStateMutable.value?.data == true val alreadyMuted = muteStateMutable.value?.data == true
if (alreadyMuted) { viewModelScope.launch {
try {
val relationship = if (alreadyMuted) {
mastodonApi.unmuteAccount(accountId) mastodonApi.unmuteAccount(accountId)
} else { } else {
mastodonApi.muteAccount(accountId) mastodonApi.muteAccount(accountId)
} }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val muting = relationship.muting val muting = relationship.muting
muteStateMutable.value = Success(muting) muteStateMutable.value = Success(muting)
if (muting) { if (muting) {
eventHub.dispatch(MuteEvent(accountId)) eventHub.dispatch(MuteEvent(accountId))
} }
}, } catch (t: Throwable) {
{ error -> muteStateMutable.value = Error(false, t.message)
muteStateMutable.value = Error(false, error.message) }
} }
).autoDispose()
muteStateMutable.value = Loading() muteStateMutable.value = Loading()
} }
fun toggleBlock() { fun toggleBlock() {
val alreadyBlocked = blockStateMutable.value?.data == true val alreadyBlocked = blockStateMutable.value?.data == true
if (alreadyBlocked) { viewModelScope.launch {
try {
val relationship = if (alreadyBlocked) {
mastodonApi.unblockAccount(accountId) mastodonApi.unblockAccount(accountId)
} else { } else {
mastodonApi.blockAccount(accountId) mastodonApi.blockAccount(accountId)
} }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val blocking = relationship.blocking val blocking = relationship.blocking
blockStateMutable.value = Success(blocking) blockStateMutable.value = Success(blocking)
if (blocking) { if (blocking) {
eventHub.dispatch(BlockEvent(accountId)) eventHub.dispatch(BlockEvent(accountId))
} }
}, } catch (t: Throwable) {
{ error -> blockStateMutable.value = Error(false, t.message)
blockStateMutable.value = Error(false, error.message) }
} }
)
.autoDispose()
blockStateMutable.value = Loading() blockStateMutable.value = Loading()
} }

View file

@ -32,6 +32,7 @@ import com.keylesspalace.tusky.util.toViewData
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class SearchViewModel @Inject constructor( class SearchViewModel @Inject constructor(
@ -169,16 +170,20 @@ class SearchViewModel @Inject constructor(
} }
fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) { fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) {
viewModelScope.launch {
timelineCases.mute(accountId, notifications, duration) timelineCases.mute(accountId, notifications, duration)
} }
}
fun pinAccount(status: Status, isPin: Boolean) { fun pinAccount(status: Status, isPin: Boolean) {
timelineCases.pin(status.id, isPin) timelineCases.pin(status.id, isPin)
} }
fun blockAccount(accountId: String) { fun blockAccount(accountId: String) {
viewModelScope.launch {
timelineCases.block(accountId) timelineCases.block(accountId)
} }
}
fun deleteStatus(id: String): Single<DeletedStatus> { fun deleteStatus(id: String): Single<DeletedStatus> {
return timelineCases.delete(id) return timelineCases.delete(id)

View file

@ -133,20 +133,18 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
} }
override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) { override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) {
lifecycleScope.launch {
try {
if (!mute) { if (!mute) {
api.unmuteAccount(id) api.unmuteAccount(id)
} else { } else {
api.muteAccount(id, notifications) api.muteAccount(id, notifications)
} }
.autoDispose(from(this))
.subscribe(
{
onMuteSuccess(mute, id, position, notifications) onMuteSuccess(mute, id, position, notifications)
}, } catch (_: Throwable) {
{
onMuteFailure(mute, id, notifications) onMuteFailure(mute, id, notifications)
} }
) }
} }
private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) { private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) {
@ -181,20 +179,18 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
} }
override fun onBlock(block: Boolean, id: String, position: Int) { override fun onBlock(block: Boolean, id: String, position: Int) {
lifecycleScope.launch {
try {
if (!block) { if (!block) {
api.unblockAccount(id) api.unblockAccount(id)
} else { } else {
api.blockAccount(id) api.blockAccount(id)
} }
.autoDispose(from(this))
.subscribe(
{
onBlockSuccess(block, id, position) onBlockSuccess(block, id, position)
}, } catch (_: Throwable) {
{
onBlockFailure(block, id) onBlockFailure(block, id)
} }
) }
} }
private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) {

View file

@ -62,8 +62,6 @@ import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.lang.IllegalStateException
import java.util.LinkedHashSet
import javax.inject.Inject import javax.inject.Inject
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an /* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
@ -311,16 +309,20 @@ abstract class SFragment : Fragment(), Injectable {
private fun onMute(accountId: String, accountUsername: String) { private fun onMute(accountId: String, accountUsername: String) {
showMuteAccountDialog(this.requireActivity(), accountUsername) { notifications: Boolean?, duration: Int? -> showMuteAccountDialog(this.requireActivity(), accountUsername) { notifications: Boolean?, duration: Int? ->
lifecycleScope.launch {
timelineCases.mute(accountId, notifications == true, duration) timelineCases.mute(accountId, notifications == true, duration)
} }
} }
}
private fun onBlock(accountId: String, accountUsername: String) { private fun onBlock(accountId: String, accountUsername: String) {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.dialog_block_warning, accountUsername)) .setMessage(getString(R.string.dialog_block_warning, accountUsername))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
lifecycleScope.launch {
timelineCases.block(accountId) timelineCases.block(accountId)
} }
}
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }

View file

@ -359,39 +359,39 @@ interface MastodonApi {
@FormUrlEncoded @FormUrlEncoded
@POST("api/v1/accounts/{id}/follow") @POST("api/v1/accounts/{id}/follow")
fun followAccount( suspend fun followAccount(
@Path("id") accountId: String, @Path("id") accountId: String,
@Field("reblogs") showReblogs: Boolean? = null, @Field("reblogs") showReblogs: Boolean? = null,
@Field("notify") notify: Boolean? = null @Field("notify") notify: Boolean? = null
): Single<Relationship> ): Relationship
@POST("api/v1/accounts/{id}/unfollow") @POST("api/v1/accounts/{id}/unfollow")
fun unfollowAccount( suspend fun unfollowAccount(
@Path("id") accountId: String @Path("id") accountId: String
): Single<Relationship> ): Relationship
@POST("api/v1/accounts/{id}/block") @POST("api/v1/accounts/{id}/block")
fun blockAccount( suspend fun blockAccount(
@Path("id") accountId: String @Path("id") accountId: String
): Single<Relationship> ): Relationship
@POST("api/v1/accounts/{id}/unblock") @POST("api/v1/accounts/{id}/unblock")
fun unblockAccount( suspend fun unblockAccount(
@Path("id") accountId: String @Path("id") accountId: String
): Single<Relationship> ): Relationship
@FormUrlEncoded @FormUrlEncoded
@POST("api/v1/accounts/{id}/mute") @POST("api/v1/accounts/{id}/mute")
fun muteAccount( suspend fun muteAccount(
@Path("id") accountId: String, @Path("id") accountId: String,
@Field("notifications") notifications: Boolean? = null, @Field("notifications") notifications: Boolean? = null,
@Field("duration") duration: Int? = null @Field("duration") duration: Int? = null
): Single<Relationship> ): Relationship
@POST("api/v1/accounts/{id}/unmute") @POST("api/v1/accounts/{id}/unmute")
fun unmuteAccount( suspend fun unmuteAccount(
@Path("id") accountId: String @Path("id") accountId: String
): Single<Relationship> ): Relationship
@GET("api/v1/accounts/relationships") @GET("api/v1/accounts/relationships")
fun relationships( fun relationships(
@ -399,14 +399,14 @@ interface MastodonApi {
): Single<List<Relationship>> ): Single<List<Relationship>>
@POST("api/v1/pleroma/accounts/{id}/subscribe") @POST("api/v1/pleroma/accounts/{id}/subscribe")
fun subscribeAccount( suspend fun subscribeAccount(
@Path("id") accountId: String @Path("id") accountId: String
): Single<Relationship> ): Relationship
@POST("api/v1/pleroma/accounts/{id}/unsubscribe") @POST("api/v1/pleroma/accounts/{id}/unsubscribe")
fun unsubscribeAccount( suspend fun unsubscribeAccount(
@Path("id") accountId: String @Path("id") accountId: String
): Single<Relationship> ): Relationship
@GET("api/v1/blocks") @GET("api/v1/blocks")
suspend fun blocks( suspend fun blocks(

View file

@ -33,7 +33,6 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.getServerErrorMessage 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 javax.inject.Inject import javax.inject.Inject
/** /**
@ -95,30 +94,22 @@ class TimelineCases @Inject constructor(
} }
} }
fun mute(statusId: String, notifications: Boolean, duration: Int?) { suspend fun mute(statusId: String, notifications: Boolean, duration: Int?) {
try {
mastodonApi.muteAccount(statusId, notifications, duration) mastodonApi.muteAccount(statusId, notifications, duration)
.subscribe(
{
eventHub.dispatch(MuteEvent(statusId)) eventHub.dispatch(MuteEvent(statusId))
}, } catch (t: Throwable) {
{ t -> Log.w(TAG, "Failed to mute account", t)
Log.w("Failed to mute account", t)
} }
)
.addTo(cancelDisposable)
} }
fun block(statusId: String) { suspend fun block(statusId: String) {
try {
mastodonApi.blockAccount(statusId) mastodonApi.blockAccount(statusId)
.subscribe(
{
eventHub.dispatch(BlockEvent(statusId)) eventHub.dispatch(BlockEvent(statusId))
}, } catch (t: Throwable) {
{ t -> Log.w(TAG, "Failed to block account", t)
Log.w("Failed to block account", t)
} }
)
.addTo(cancelDisposable)
} }
fun delete(statusId: String): Single<DeletedStatus> { fun delete(statusId: String): Single<DeletedStatus> {
@ -132,7 +123,7 @@ class TimelineCases @Inject constructor(
// 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 -> .doOnError { e ->
Log.w("Failed to change pin state", e) Log.w(TAG, "Failed to change pin state", e)
} }
.onErrorResumeNext(::convertError) .onErrorResumeNext(::convertError)
.doAfterSuccess { .doAfterSuccess {
@ -153,6 +144,10 @@ class TimelineCases @Inject constructor(
private fun <T : Any> convertError(e: Throwable): Single<T> { private fun <T : Any> convertError(e: Throwable): Single<T> {
return Single.error(TimelineError(e.getServerErrorMessage())) return Single.error(TimelineError(e.getServerErrorMessage()))
} }
companion object {
private const val TAG = "TimelineCases"
}
} }
class TimelineError(message: String?) : RuntimeException(message) class TimelineError(message: String?) : RuntimeException(message)