diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt index 976253fca..af665e7c8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt @@ -2,6 +2,7 @@ package com.keylesspalace.tusky.components.account import android.util.Log import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.BlockEvent @@ -17,19 +18,17 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource -import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.disposables.Disposable +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit import javax.inject.Inject class AccountViewModel @Inject constructor( private val mastodonApi: MastodonApi, private val eventHub: EventHub, private val accountManager: AccountManager -) : RxAwareViewModel() { +) : ViewModel() { val accountData = MutableLiveData>() val relationshipData = MutableLiveData>() @@ -42,7 +41,7 @@ class AccountViewModel @Inject constructor( lateinit var accountId: String var isSelf = false - private var noteDisposable: Disposable? = null + private var noteUpdateJob: Job? = null init { viewModelScope.launch { @@ -59,21 +58,22 @@ class AccountViewModel @Inject constructor( isDataLoading = true accountData.postValue(Loading()) - mastodonApi.account(accountId) - .subscribe( - { account -> - accountData.postValue(Success(account)) - isDataLoading = false - isRefreshing.postValue(false) - }, - { t -> - Log.w(TAG, "failed obtaining account", t) - accountData.postValue(Error()) - isDataLoading = false - isRefreshing.postValue(false) - } - ) - .autoDispose() + viewModelScope.launch { + mastodonApi.account(accountId) + .fold( + { account -> + accountData.postValue(Success(account)) + isDataLoading = false + isRefreshing.postValue(false) + }, + { t -> + Log.w(TAG, "failed obtaining account", t) + accountData.postValue(Error(cause = t)) + isDataLoading = false + isRefreshing.postValue(false) + } + ) + } } } @@ -81,17 +81,18 @@ class AccountViewModel @Inject constructor( if (relationshipData.value == null || reload) { relationshipData.postValue(Loading()) - mastodonApi.relationships(listOf(accountId)) - .subscribe( - { relationships -> - relationshipData.postValue(Success(relationships[0])) - }, - { t -> - Log.w(TAG, "failed obtaining relationships", t) - relationshipData.postValue(Error()) - } - ) - .autoDispose() + viewModelScope.launch { + mastodonApi.relationships(listOf(accountId)) + .fold( + { relationships -> + relationshipData.postValue(Success(relationships[0])) + }, + { t -> + Log.w(TAG, "failed obtaining relationships", t) + relationshipData.postValue(Error(cause = t)) + } + ) + } } } @@ -212,75 +213,71 @@ class AccountViewModel @Inject constructor( relationshipData.postValue(Loading(newRelation)) } - try { - val relationship = when (relationshipAction) { - RelationShipAction.FOLLOW -> mastodonApi.followAccount( - accountId, - showReblogs = parameter ?: true - ) - RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) - RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) - RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) - RelationShipAction.MUTE -> mastodonApi.muteAccount( - accountId, - parameter ?: true, - duration - ) - RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) - RelationShipAction.SUBSCRIBE -> { - if (isMastodon) { - mastodonApi.followAccount(accountId, notify = true) - } else { - mastodonApi.subscribeAccount(accountId) - } - } - RelationShipAction.UNSUBSCRIBE -> { - if (isMastodon) { - mastodonApi.followAccount(accountId, notify = false) - } else { - mastodonApi.unsubscribeAccount(accountId) - } + val relationshipCall = when (relationshipAction) { + RelationShipAction.FOLLOW -> mastodonApi.followAccount( + accountId, + showReblogs = parameter ?: true + ) + RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) + RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) + RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) + RelationShipAction.MUTE -> mastodonApi.muteAccount( + accountId, + parameter ?: true, + duration + ) + RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) + RelationShipAction.SUBSCRIBE -> { + if (isMastodon) { + mastodonApi.followAccount(accountId, notify = true) + } else { + mastodonApi.subscribeAccount(accountId) } } - - relationshipData.postValue(Success(relationship)) - - when (relationshipAction) { - RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId)) - RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId)) - RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId)) - else -> { + RelationShipAction.UNSUBSCRIBE -> { + if (isMastodon) { + mastodonApi.followAccount(accountId, notify = false) + } else { + mastodonApi.unsubscribeAccount(accountId) } } - } catch (_: Throwable) { - relationshipData.postValue(Error(relation)) } + + relationshipCall.fold( + { relationship -> + relationshipData.postValue(Success(relationship)) + + when (relationshipAction) { + RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId)) + RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId)) + RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId)) + else -> { } + } + }, + { t -> + Log.w(TAG, "failed loading relationship", t) + relationshipData.postValue(Error(relation, cause = t)) + } + ) } fun noteChanged(newNote: String) { noteSaved.postValue(false) - noteDisposable?.dispose() - noteDisposable = Single.timer(1500, TimeUnit.MILLISECONDS) - .flatMap { - mastodonApi.updateAccountNote(accountId, newNote) - } - .doOnSuccess { - noteSaved.postValue(true) - } - .delay(4, TimeUnit.SECONDS) - .subscribe( - { - noteSaved.postValue(false) - }, - { - Log.e(TAG, "Error updating note", it) - } - ) - } - - override fun onCleared() { - super.onCleared() - noteDisposable?.dispose() + noteUpdateJob?.cancel() + noteUpdateJob = viewModelScope.launch { + delay(1500) + mastodonApi.updateAccountNote(accountId, newNote) + .fold( + { + noteSaved.postValue(true) + delay(4000) + noteSaved.postValue(false) + }, + { t -> + Log.w(TAG, "Error updating note", t) + } + ) + } } fun refresh() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt index eab95b13a..d18ae4fd5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt @@ -27,6 +27,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator +import at.connyduck.calladapter.networkresult.fold import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.autoDispose import com.google.android.material.snackbar.Snackbar @@ -348,12 +349,12 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } private fun fetchRelationships(ids: List) { - api.relationships(ids) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe(::onFetchRelationshipsSuccess) { throwable -> - Log.e(TAG, "Fetch failure for relationships of accounts: $ids", throwable) - } + lifecycleScope.launch { + api.relationships(ids) + .fold(::onFetchRelationshipsSuccess) { throwable -> + Log.e(TAG, "Fetch failure for relationships of accounts: $ids", throwable) + } + } } private fun onFetchRelationshipsSuccess(relationships: List) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index fe9215a43..f30726a23 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -17,11 +17,13 @@ package com.keylesspalace.tusky.components.report import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn import androidx.paging.map +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MuteEvent @@ -33,11 +35,8 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource -import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.toViewData -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flatMapLatest @@ -48,7 +47,7 @@ import javax.inject.Inject class ReportViewModel @Inject constructor( private val mastodonApi: MastodonApi, private val eventHub: EventHub -) : RxAwareViewModel() { +) : ViewModel() { private val navigationMutable = MutableLiveData() val navigation: LiveData = navigationMutable @@ -128,10 +127,8 @@ class ReportViewModel @Inject constructor( val ids = listOf(accountId) muteStateMutable.value = Loading() blockStateMutable.value = Loading() - mastodonApi.relationships(ids) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( + viewModelScope.launch { + mastodonApi.relationships(ids).fold( { data -> updateRelationship(data.getOrNull(0)) }, @@ -139,7 +136,7 @@ class ReportViewModel @Inject constructor( updateRelationship(null) } ) - .autoDispose() + } } private fun updateRelationship(relationship: Relationship?) { @@ -155,21 +152,22 @@ class ReportViewModel @Inject constructor( fun toggleMute() { val alreadyMuted = muteStateMutable.value?.data == true viewModelScope.launch { - try { - val relationship = if (alreadyMuted) { - mastodonApi.unmuteAccount(accountId) - } else { - mastodonApi.muteAccount(accountId) + if (alreadyMuted) { + mastodonApi.unmuteAccount(accountId) + } else { + mastodonApi.muteAccount(accountId) + }.fold( + { relationship -> + val muting = relationship.muting + muteStateMutable.value = Success(muting) + if (muting) { + eventHub.dispatch(MuteEvent(accountId)) + } + }, + { t -> + muteStateMutable.value = Error(false, t.message) } - - val muting = relationship.muting - muteStateMutable.value = Success(muting) - if (muting) { - eventHub.dispatch(MuteEvent(accountId)) - } - } catch (t: Throwable) { - muteStateMutable.value = Error(false, t.message) - } + ) } muteStateMutable.value = Loading() @@ -178,39 +176,33 @@ class ReportViewModel @Inject constructor( fun toggleBlock() { val alreadyBlocked = blockStateMutable.value?.data == true viewModelScope.launch { - try { - val relationship = if (alreadyBlocked) { - mastodonApi.unblockAccount(accountId) - } else { - mastodonApi.blockAccount(accountId) - } - + if (alreadyBlocked) { + mastodonApi.unblockAccount(accountId) + } else { + mastodonApi.blockAccount(accountId) + }.fold({ relationship -> val blocking = relationship.blocking blockStateMutable.value = Success(blocking) if (blocking) { eventHub.dispatch(BlockEvent(accountId)) } - } catch (t: Throwable) { + }, { t -> blockStateMutable.value = Error(false, t.message) - } + }) } blockStateMutable.value = Loading() } fun doReport() { reportingStateMutable.value = Loading() - mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { + viewModelScope.launch { + mastodonApi.report(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null) + .fold({ reportingStateMutable.value = Success(true) - }, - { error -> + }, { error -> reportingStateMutable.value = Error(cause = error) - } - ) - .autoDispose() + }) + } } fun checkClickedUrl(url: String?) { diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 9a10a6e71..ae74ceecc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -344,9 +344,9 @@ interface MastodonApi { ): NetworkResult> @GET("api/v1/accounts/{id}") - fun account( + suspend fun account( @Path("id") accountId: String - ): Single + ): NetworkResult /** * Method to fetch statuses for the specified account. @@ -386,22 +386,22 @@ interface MastodonApi { @Path("id") accountId: String, @Field("reblogs") showReblogs: Boolean? = null, @Field("notify") notify: Boolean? = null - ): Relationship + ): NetworkResult @POST("api/v1/accounts/{id}/unfollow") suspend fun unfollowAccount( @Path("id") accountId: String - ): Relationship + ): NetworkResult @POST("api/v1/accounts/{id}/block") suspend fun blockAccount( @Path("id") accountId: String - ): Relationship + ): NetworkResult @POST("api/v1/accounts/{id}/unblock") suspend fun unblockAccount( @Path("id") accountId: String - ): Relationship + ): NetworkResult @FormUrlEncoded @POST("api/v1/accounts/{id}/mute") @@ -409,27 +409,27 @@ interface MastodonApi { @Path("id") accountId: String, @Field("notifications") notifications: Boolean? = null, @Field("duration") duration: Int? = null - ): Relationship + ): NetworkResult @POST("api/v1/accounts/{id}/unmute") suspend fun unmuteAccount( @Path("id") accountId: String - ): Relationship + ): NetworkResult @GET("api/v1/accounts/relationships") - fun relationships( + suspend fun relationships( @Query("id[]") accountIds: List - ): Single> + ): NetworkResult> @POST("api/v1/pleroma/accounts/{id}/subscribe") suspend fun subscribeAccount( @Path("id") accountId: String - ): Relationship + ): NetworkResult @POST("api/v1/pleroma/accounts/{id}/unsubscribe") suspend fun unsubscribeAccount( @Path("id") accountId: String - ): Relationship + ): NetworkResult @GET("api/v1/blocks") suspend fun blocks( @@ -677,12 +677,12 @@ interface MastodonApi { @FormUrlEncoded @POST("api/v1/reports") - fun reportObservable( + fun report( @Field("account_id") accountId: String, @Field("status_ids[]") statusIds: List, @Field("comment") comment: String, @Field("forward") isNotifyRemote: Boolean? - ): Single + ): NetworkResult @GET("api/v1/accounts/{id}/statuses") fun accountStatusesObservable( @@ -721,10 +721,10 @@ interface MastodonApi { @FormUrlEncoded @POST("api/v1/accounts/{id}/note") - fun updateAccountNote( + suspend fun updateAccountNote( @Path("id") accountId: String, @Field("comment") note: String - ): Single + ): NetworkResult @FormUrlEncoded @POST("api/v1/push/subscription") diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt deleted file mode 100644 index 16a93486c..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.keylesspalace.tusky.util - -import androidx.annotation.CallSuper -import androidx.lifecycle.ViewModel -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.disposables.Disposable - -open class RxAwareViewModel : ViewModel() { - private val disposables = CompositeDisposable() - - fun Disposable.autoDispose() = disposables.add(this) - - @CallSuper - override fun onCleared() { - super.onCleared() - disposables.clear() - } -}