refactor: Remove rxjava3 from Mastodon API spec (#128)
Remove the rxjava3 `Single` type from the MastodonAPI definition, replacing with `Response` or `NetworkResult` as appropriate. Update callsites and tests as appropriate. This removes the need for `com.squareup.retrofit2:adapter-rxjava3`
This commit is contained in:
parent
11fecb1914
commit
c5a5540467
|
@ -23,16 +23,15 @@ import android.view.View
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.pachli.components.account.AccountActivity
|
||||
import app.pachli.components.viewthread.ViewThreadActivity
|
||||
import app.pachli.network.MastodonApi
|
||||
import app.pachli.util.looksLikeMastodonUrl
|
||||
import app.pachli.util.openLink
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||
import autodispose2.autoDispose
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/** this is the base class for all activities that open links
|
||||
|
@ -73,41 +72,34 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
return
|
||||
}
|
||||
|
||||
mastodonApi.searchObservable(
|
||||
query = url,
|
||||
resolve = true,
|
||||
).observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ (accounts, statuses) ->
|
||||
if (getCancelSearchRequested(url)) {
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
onEndSearch(url)
|
||||
|
||||
if (statuses.isNotEmpty()) {
|
||||
viewThread(statuses[0].id, statuses[0].url)
|
||||
return@subscribe
|
||||
}
|
||||
accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let { account ->
|
||||
// Some servers return (unrelated) accounts for url searches (#2804)
|
||||
// Verify that the account's url matches the query
|
||||
viewAccount(account.id)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
},
|
||||
{
|
||||
if (!getCancelSearchRequested(url)) {
|
||||
onEndSearch(url)
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onBeginSearch(url)
|
||||
|
||||
lifecycleScope.launch {
|
||||
mastodonApi.search(query = url, resolve = true).fold({ searchResult ->
|
||||
val (accounts, statuses) = searchResult
|
||||
if (getCancelSearchRequested(url)) return@fold
|
||||
onEndSearch(url)
|
||||
|
||||
statuses.firstOrNull()?.let {
|
||||
viewThread(it.id, it.url)
|
||||
return@fold
|
||||
}
|
||||
|
||||
// Some servers return (unrelated) accounts for url searches (#2804)
|
||||
// Verify that the account's url matches the query
|
||||
accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let {
|
||||
viewAccount(it.id)
|
||||
return@fold
|
||||
}
|
||||
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
}, {
|
||||
if (!getCancelSearchRequested(url)) {
|
||||
onEndSearch(url)
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
}
|
||||
},)
|
||||
}
|
||||
}
|
||||
|
||||
open fun viewThread(statusId: String, url: String?) {
|
||||
|
@ -138,14 +130,10 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getCancelSearchRequested(url: String): Boolean {
|
||||
return url != searchUrl
|
||||
}
|
||||
fun getCancelSearchRequested(url: String) = url != searchUrl
|
||||
|
||||
@VisibleForTesting
|
||||
fun isSearching(): Boolean {
|
||||
return searchUrl != null
|
||||
}
|
||||
fun isSearching() = searchUrl != null
|
||||
|
||||
@VisibleForTesting
|
||||
fun onEndSearch(url: String?) {
|
||||
|
|
|
@ -19,7 +19,6 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
|
@ -55,12 +54,9 @@ import app.pachli.util.show
|
|||
import app.pachli.util.viewBinding
|
||||
import app.pachli.view.EndlessOnScrollListener
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
@ -258,13 +254,12 @@ class AccountListFragment :
|
|||
accountId: String,
|
||||
position: Int,
|
||||
) {
|
||||
if (accept) {
|
||||
api.authorizeFollowRequest(accountId)
|
||||
} else {
|
||||
api.rejectFollowRequest(accountId)
|
||||
}.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
if (accept) {
|
||||
api.authorizeFollowRequest(accountId)
|
||||
} else {
|
||||
api.rejectFollowRequest(accountId)
|
||||
}.fold(
|
||||
{
|
||||
onRespondToFollowRequestSuccess(position)
|
||||
},
|
||||
|
@ -277,6 +272,7 @@ class AccountListFragment :
|
|||
Log.e(TAG, "Failed to $verb account id $accountId.", throwable)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRespondToFollowRequestSuccess(position: Int) {
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -20,11 +19,8 @@ import app.pachli.util.show
|
|||
import app.pachli.util.viewBinding
|
||||
import app.pachli.view.EndlessOnScrollListener
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -101,23 +97,16 @@ class InstanceListFragment :
|
|||
binding.recyclerView.post { adapter.bottomLoading = true }
|
||||
}
|
||||
|
||||
api.domainBlocks(id, bottomId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ response ->
|
||||
val instances = response.body()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val response = api.domainBlocks(id, bottomId)
|
||||
val instances = response.body()
|
||||
|
||||
if (response.isSuccessful && instances != null) {
|
||||
onFetchInstancesSuccess(instances, response.headers()["Link"])
|
||||
} else {
|
||||
onFetchInstancesFailure(Exception(response.message()))
|
||||
}
|
||||
},
|
||||
{ throwable ->
|
||||
onFetchInstancesFailure(throwable)
|
||||
},
|
||||
)
|
||||
if (response.isSuccessful && instances != null) {
|
||||
onFetchInstancesSuccess(instances, response.headers()["Link"])
|
||||
} else {
|
||||
onFetchInstancesFailure(Exception(response.message()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
|
||||
|
@ -158,6 +147,6 @@ class InstanceListFragment :
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "InstanceList" // logging tag
|
||||
private const val TAG = "InstanceList"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,6 @@ import kotlinx.coroutines.flow.onStart
|
|||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
@ -436,9 +435,9 @@ class NotificationsViewModel @Inject constructor(
|
|||
try {
|
||||
when (action) {
|
||||
is NotificationAction.AcceptFollowRequest ->
|
||||
timelineCases.acceptFollowRequest(action.accountId).await()
|
||||
timelineCases.acceptFollowRequest(action.accountId)
|
||||
is NotificationAction.RejectFollowRequest ->
|
||||
timelineCases.rejectFollowRequest(action.accountId).await()
|
||||
timelineCases.rejectFollowRequest(action.accountId)
|
||||
}
|
||||
uiSuccess.emit(NotificationActionSuccess.from(action))
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -43,7 +43,10 @@ class StatusesPagingSource(
|
|||
withContext(Dispatchers.IO) {
|
||||
val initialStatus = async { getSingleStatus(key) }
|
||||
val additionalStatuses = async { getStatusList(maxId = key, limit = params.loadSize - 1) }
|
||||
listOf(initialStatus.await()) + additionalStatuses.await()
|
||||
buildList {
|
||||
initialStatus.await()?.let { this.add(it) }
|
||||
additionalStatuses.await()?.let { this.addAll(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val maxId = if (params is LoadParams.Refresh || params is LoadParams.Append) {
|
||||
|
@ -58,7 +61,7 @@ class StatusesPagingSource(
|
|||
null
|
||||
}
|
||||
|
||||
getStatusList(minId = minId, maxId = maxId, limit = params.loadSize)
|
||||
getStatusList(minId = minId, maxId = maxId, limit = params.loadSize) ?: emptyList()
|
||||
}
|
||||
return LoadResult.Page(
|
||||
data = result,
|
||||
|
@ -71,18 +74,18 @@ class StatusesPagingSource(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getSingleStatus(statusId: String): Status {
|
||||
return mastodonApi.statusObservable(statusId).await()
|
||||
private suspend fun getSingleStatus(statusId: String): Status? {
|
||||
return mastodonApi.status(statusId).getOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List<Status> {
|
||||
return mastodonApi.accountStatusesObservable(
|
||||
private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List<Status>? {
|
||||
return mastodonApi.accountStatuses(
|
||||
accountId = accountId,
|
||||
maxId = maxId,
|
||||
sinceId = null,
|
||||
minId = minId,
|
||||
limit = limit,
|
||||
excludeReblogs = true,
|
||||
).await()
|
||||
).body()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,11 @@
|
|||
|
||||
package app.pachli.components.scheduled
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import app.pachli.entity.ScheduledStatus
|
||||
import app.pachli.network.MastodonApi
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import at.connyduck.calladapter.networkresult.getOrElse
|
||||
|
||||
class ScheduledStatusPagingSourceFactory(
|
||||
private val mastodonApi: MastodonApi,
|
||||
|
@ -59,21 +58,12 @@ class ScheduledStatusPagingSource(
|
|||
nextKey = scheduledStatusesCache.lastOrNull()?.id,
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
val result = mastodonApi.scheduledStatuses(
|
||||
maxId = params.key,
|
||||
limit = params.loadSize,
|
||||
).await()
|
||||
val result = mastodonApi.scheduledStatuses(
|
||||
maxId = params.key,
|
||||
limit = params.loadSize,
|
||||
).getOrElse { return LoadResult.Error(it) }
|
||||
|
||||
LoadResult.Page(
|
||||
data = result,
|
||||
prevKey = null,
|
||||
nextKey = result.lastOrNull()?.id,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("ScheduledStatuses", "Error loading scheduled statuses", e)
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
LoadResult.Page(data = result, prevKey = null, nextKey = result.lastOrNull()?.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import androidx.paging.PagingState
|
|||
import app.pachli.components.search.SearchType
|
||||
import app.pachli.entity.SearchResult
|
||||
import app.pachli.network.MastodonApi
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import at.connyduck.calladapter.networkresult.getOrElse
|
||||
|
||||
class SearchPagingSource<T : Any>(
|
||||
private val mastodonApi: MastodonApi,
|
||||
|
@ -53,31 +53,27 @@ class SearchPagingSource<T : Any>(
|
|||
|
||||
val currentKey = params.key ?: 0
|
||||
|
||||
try {
|
||||
val data = mastodonApi.searchObservable(
|
||||
query = searchRequest,
|
||||
type = searchType.apiParameter,
|
||||
resolve = true,
|
||||
limit = params.loadSize,
|
||||
offset = currentKey,
|
||||
following = false,
|
||||
).await()
|
||||
val data = mastodonApi.search(
|
||||
query = searchRequest,
|
||||
type = searchType.apiParameter,
|
||||
resolve = true,
|
||||
limit = params.loadSize,
|
||||
offset = currentKey,
|
||||
following = false,
|
||||
).getOrElse { return LoadResult.Error(it) }
|
||||
|
||||
val res = parser(data)
|
||||
val res = parser(data)
|
||||
|
||||
val nextKey = if (res.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
currentKey + res.size
|
||||
}
|
||||
|
||||
return LoadResult.Page(
|
||||
data = res,
|
||||
prevKey = null,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(e)
|
||||
val nextKey = if (res.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
currentKey + res.size
|
||||
}
|
||||
|
||||
return LoadResult.Page(
|
||||
data = res,
|
||||
prevKey = null,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ import okhttp3.OkHttp
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.create
|
||||
import java.net.IDN
|
||||
|
@ -115,7 +114,6 @@ class NetworkModule {
|
|||
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
|
||||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ import app.pachli.entity.TrendingTag
|
|||
import app.pachli.entity.TrendsLink
|
||||
import app.pachli.util.HttpHeaderLink
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.ResponseBody
|
||||
|
@ -312,10 +311,10 @@ interface MastodonApi {
|
|||
): NetworkResult<Status>
|
||||
|
||||
@GET("api/v1/scheduled_statuses")
|
||||
fun scheduledStatuses(
|
||||
suspend fun scheduledStatuses(
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("max_id") maxId: String? = null,
|
||||
): Single<List<ScheduledStatus>>
|
||||
): NetworkResult<List<ScheduledStatus>>
|
||||
|
||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||
suspend fun deleteScheduledStatus(
|
||||
|
@ -378,9 +377,11 @@ interface MastodonApi {
|
|||
/**
|
||||
* Method to fetch statuses for the specified account.
|
||||
* @param accountId ID for account for which statuses will be requested
|
||||
* @param maxId Only statuses with ID less than maxID will be returned
|
||||
* @param sinceId Only statuses with ID bigger than sinceID will be returned
|
||||
* @param limit Limit returned statuses (current API limits: default - 20, max - 40)
|
||||
* @param maxId Only statuses with ID less than maxId will be returned
|
||||
* @param sinceId Only statuses with ID bigger than sinceId will be returned
|
||||
* @param minId Only statuses with ID greater than minId will be returned
|
||||
* @param limit Limit returned statuses
|
||||
* @param excludeReblogs only return statuses that are not reblogs
|
||||
* @param excludeReplies only return statuses that are no replies
|
||||
* @param onlyMedia only return statuses that have media attached
|
||||
*/
|
||||
|
@ -391,6 +392,7 @@ interface MastodonApi {
|
|||
@Query("since_id") sinceId: String? = null,
|
||||
@Query("min_id") minId: String? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("exclude_reblogs") excludeReblogs: Boolean? = null,
|
||||
@Query("exclude_replies") excludeReplies: Boolean? = null,
|
||||
@Query("only_media") onlyMedia: Boolean? = null,
|
||||
@Query("pinned") pinned: Boolean? = null,
|
||||
|
@ -470,11 +472,11 @@ interface MastodonApi {
|
|||
): Response<List<TimelineAccount>>
|
||||
|
||||
@GET("api/v1/domain_blocks")
|
||||
fun domainBlocks(
|
||||
suspend fun domainBlocks(
|
||||
@Query("max_id") maxId: String? = null,
|
||||
@Query("since_id") sinceId: String? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
): Single<Response<List<String>>>
|
||||
): Response<List<String>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/domain_blocks")
|
||||
|
@ -509,14 +511,14 @@ interface MastodonApi {
|
|||
): Response<List<TimelineAccount>>
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/authorize")
|
||||
fun authorizeFollowRequest(
|
||||
suspend fun authorizeFollowRequest(
|
||||
@Path("id") accountId: String,
|
||||
): Single<Relationship>
|
||||
): NetworkResult<Relationship>
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/reject")
|
||||
fun rejectFollowRequest(
|
||||
suspend fun rejectFollowRequest(
|
||||
@Path("id") accountId: String,
|
||||
): Single<Relationship>
|
||||
): NetworkResult<Relationship>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/apps")
|
||||
|
@ -716,34 +718,19 @@ interface MastodonApi {
|
|||
@Field("forward") isNotifyRemote: Boolean?,
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
fun accountStatusesObservable(
|
||||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("min_id") minId: String?,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("exclude_reblogs") excludeReblogs: Boolean?,
|
||||
): Single<List<Status>>
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
fun statusObservable(
|
||||
@Path("id") statusId: String,
|
||||
): Single<Status>
|
||||
|
||||
@GET("api/v2/search")
|
||||
fun searchObservable(
|
||||
suspend fun search(
|
||||
@Query("q") query: String?,
|
||||
@Query("type") type: String? = null,
|
||||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("offset") offset: Int? = null,
|
||||
@Query("following") following: Boolean? = null,
|
||||
): Single<SearchResult>
|
||||
): NetworkResult<SearchResult>
|
||||
|
||||
@GET("api/v2/search")
|
||||
fun searchSync(
|
||||
@Query("q") query: String?,
|
||||
@Query("q") query: String,
|
||||
@Query("type") type: String? = null,
|
||||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
|
|
|
@ -36,7 +36,6 @@ import at.connyduck.calladapter.networkresult.NetworkResult
|
|||
import at.connyduck.calladapter.networkresult.fold
|
||||
import at.connyduck.calladapter.networkresult.onFailure
|
||||
import at.connyduck.calladapter.networkresult.onSuccess
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineCases @Inject constructor(
|
||||
|
@ -132,11 +131,11 @@ class TimelineCases @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun acceptFollowRequest(accountId: String): Single<Relationship> {
|
||||
suspend fun acceptFollowRequest(accountId: String): NetworkResult<Relationship> {
|
||||
return mastodonApi.authorizeFollowRequest(accountId)
|
||||
}
|
||||
|
||||
fun rejectFollowRequest(accountId: String): Single<Relationship> {
|
||||
suspend fun rejectFollowRequest(accountId: String): NetworkResult<Relationship> {
|
||||
return mastodonApi.rejectFollowRequest(accountId)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,14 +18,16 @@
|
|||
package app.pachli
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import app.pachli.components.timeline.MainCoroutineRule
|
||||
import app.pachli.entity.SearchResult
|
||||
import app.pachli.entity.Status
|
||||
import app.pachli.entity.TimelineAccount
|
||||
import app.pachli.network.MastodonApi
|
||||
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
@ -37,21 +39,25 @@ import org.mockito.Mockito.eq
|
|||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class BottomSheetActivityTest {
|
||||
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainCoroutineRule = MainCoroutineRule(dispatcher = StandardTestDispatcher())
|
||||
|
||||
private lateinit var activity: FakeBottomSheetActivity
|
||||
private lateinit var apiMock: MastodonApi
|
||||
private val accountQuery = "http://mastodon.foo.bar/@User"
|
||||
private val statusQuery = "http://mastodon.foo.bar/@User/345678"
|
||||
private val nonexistentStatusQuery = "http://mastodon.foo.bar/@User/345678000"
|
||||
private val nonMastodonQuery = "http://medium.com/@correspondent/345678"
|
||||
private val emptyCallback = Single.just(SearchResult(emptyList(), emptyList(), emptyList()))
|
||||
private val testScheduler = TestScheduler()
|
||||
private val emptyResponse = NetworkResult.success(
|
||||
SearchResult(emptyList(), emptyList(), emptyList()),
|
||||
)
|
||||
|
||||
private val account = TimelineAccount(
|
||||
id = "1",
|
||||
|
@ -62,7 +68,9 @@ class BottomSheetActivityTest {
|
|||
url = "http://mastodon.foo.bar/@User",
|
||||
avatar = "",
|
||||
)
|
||||
private val accountSingle = Single.just(SearchResult(listOf(account), emptyList(), emptyList()))
|
||||
private val accountResponse = NetworkResult.success(
|
||||
SearchResult(listOf(account), emptyList(), emptyList()),
|
||||
)
|
||||
|
||||
private val status = Status(
|
||||
id = "1",
|
||||
|
@ -95,31 +103,30 @@ class BottomSheetActivityTest {
|
|||
language = null,
|
||||
filtered = null,
|
||||
)
|
||||
private val statusSingle = Single.just(SearchResult(emptyList(), listOf(status), emptyList()))
|
||||
private val statusResponse = NetworkResult.success(
|
||||
SearchResult(emptyList(), listOf(status), emptyList()),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
|
||||
RxAndroidPlugins.setMainThreadSchedulerHandler { testScheduler }
|
||||
|
||||
apiMock = mock {
|
||||
on { searchObservable(eq(accountQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountSingle
|
||||
on { searchObservable(eq(statusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn statusSingle
|
||||
on { searchObservable(eq(nonexistentStatusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountSingle
|
||||
on { searchObservable(eq(nonMastodonQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn emptyCallback
|
||||
onBlocking { search(eq(accountQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResponse
|
||||
onBlocking { search(eq(statusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn statusResponse
|
||||
onBlocking { search(eq(nonexistentStatusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResponse
|
||||
onBlocking { search(eq(nonMastodonQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn emptyResponse
|
||||
}
|
||||
|
||||
activity = FakeBottomSheetActivity(apiMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() {
|
||||
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() = runTest {
|
||||
activity.onBeginSearch("https://mastodon.foo.bar/@User")
|
||||
assertTrue(activity.isSearching())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() {
|
||||
fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() = runTest {
|
||||
val validUrl = "https://mastodon.foo.bar/@User"
|
||||
activity.onBeginSearch(validUrl)
|
||||
activity.onEndSearch(validUrl)
|
||||
|
@ -127,7 +134,7 @@ class BottomSheetActivityTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun beginEndSearch_setIsSearching_doesNotCancelSearchWhenResponseFromPreviousSearchIsReceived() {
|
||||
fun beginEndSearch_setIsSearching_doesNotCancelSearchWhenResponseFromPreviousSearchIsReceived() = runTest {
|
||||
val validUrl = "https://mastodon.foo.bar/@User"
|
||||
val invalidUrl = ""
|
||||
|
||||
|
@ -137,7 +144,7 @@ class BottomSheetActivityTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun cancelActiveSearch() {
|
||||
fun cancelActiveSearch() = runTest {
|
||||
val url = "https://mastodon.foo.bar/@User"
|
||||
|
||||
activity.onBeginSearch(url)
|
||||
|
@ -146,7 +153,7 @@ class BottomSheetActivityTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun getCancelSearchRequested_detectsURL() {
|
||||
fun getCancelSearchRequested_detectsURL() = runTest {
|
||||
val firstUrl = "https://mastodon.foo.bar/@User"
|
||||
val secondUrl = "https://mastodon.foo.bar/@meh"
|
||||
|
||||
|
@ -159,46 +166,46 @@ class BottomSheetActivityTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forAccount() {
|
||||
fun search_inIdealConditions_returnsRequestedResults_forAccount() = runTest {
|
||||
activity.viewUrl(accountQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
advanceUntilIdle()
|
||||
assertEquals(account.id, activity.accountId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
|
||||
fun search_inIdealConditions_returnsRequestedResults_forStatus() = runTest {
|
||||
activity.viewUrl(statusQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
advanceUntilIdle()
|
||||
assertEquals(status.id, activity.statusId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() {
|
||||
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() = runTest {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
advanceUntilIdle()
|
||||
assertEquals(nonMastodonQuery, activity.link)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withNoResults_appliesRequestedFallbackBehavior() {
|
||||
fun search_withNoResults_appliesRequestedFallbackBehavior() = runTest {
|
||||
for (fallbackBehavior in listOf(PostLookupFallbackBehavior.OPEN_IN_BROWSER, PostLookupFallbackBehavior.DISPLAY_ERROR)) {
|
||||
activity.viewUrl(nonMastodonQuery, fallbackBehavior)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
advanceUntilIdle()
|
||||
assertEquals(nonMastodonQuery, activity.link)
|
||||
assertEquals(fallbackBehavior, activity.fallbackBehavior)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_doesNotRespectUnrelatedResult() {
|
||||
fun search_doesNotRespectUnrelatedResult() = runTest {
|
||||
activity.viewUrl(nonexistentStatusQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
advanceUntilIdle()
|
||||
assertEquals(nonexistentStatusQuery, activity.link)
|
||||
assertEquals(null, activity.accountId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forAccount() {
|
||||
fun search_withCancellation_doesNotLoadUrl_forAccount() = runTest {
|
||||
activity.viewUrl(accountQuery)
|
||||
assertTrue(activity.isSearching())
|
||||
activity.cancelActiveSearch()
|
||||
|
@ -207,21 +214,21 @@ class BottomSheetActivityTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forStatus() {
|
||||
fun search_withCancellation_doesNotLoadUrl_forStatus() = runTest {
|
||||
activity.viewUrl(accountQuery)
|
||||
activity.cancelActiveSearch()
|
||||
assertEquals(null, activity.accountId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() {
|
||||
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() = runTest {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
activity.cancelActiveSearch()
|
||||
assertEquals(null, activity.searchUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withPreviousCancellation_completes() {
|
||||
fun search_withPreviousCancellation_completes() = runTest {
|
||||
// begin/cancel account search
|
||||
activity.viewUrl(accountQuery)
|
||||
activity.cancelActiveSearch()
|
||||
|
@ -233,7 +240,7 @@ class BottomSheetActivityTest {
|
|||
assertTrue(activity.isSearching())
|
||||
|
||||
// return searchResults
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
advanceUntilIdle()
|
||||
|
||||
// ensure that the result of the status search was recorded
|
||||
// and the account search wasn't
|
||||
|
|
|
@ -19,8 +19,8 @@ package app.pachli.components.notifications
|
|||
|
||||
import app.cash.turbine.test
|
||||
import app.pachli.entity.Relationship
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.any
|
||||
|
@ -66,7 +66,7 @@ class NotificationsViewModelTestNotificationAction : NotificationsViewModelTestB
|
|||
fun `accepting follow request succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub {
|
||||
onBlocking { acceptFollowRequest(any()) } doReturn Single.just(relationship)
|
||||
onBlocking { acceptFollowRequest(any()) } doReturn NetworkResult.success(relationship)
|
||||
}
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
|
@ -105,7 +105,7 @@ class NotificationsViewModelTestNotificationAction : NotificationsViewModelTestB
|
|||
@Test
|
||||
fun `rejecting follow request succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn Single.just(relationship) }
|
||||
timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn NetworkResult.success(relationship) }
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
// When
|
||||
|
|
|
@ -137,7 +137,6 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "
|
|||
networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" }
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
retrofit-adapter-rxjava3 = { module = "com.squareup.retrofit2:adapter-rxjava3", version.ref = "retrofit" }
|
||||
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
|
||||
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
|
@ -169,7 +168,7 @@ glide = ["glide-core", "glide-okhttp3-integration", "glide-animation-plugin"]
|
|||
material-drawer = ["material-drawer-core", "material-drawer-iconics"]
|
||||
mockito = ["mockito-kotlin", "mockito-inline"]
|
||||
okhttp = ["okhttp-core", "okhttp-logging-interceptor"]
|
||||
retrofit = ["retrofit-core", "retrofit-converter-gson", "retrofit-adapter-rxjava3"]
|
||||
retrofit = ["retrofit-core", "retrofit-converter-gson"]
|
||||
room = ["androidx-room-ktx", "androidx-room-paging"]
|
||||
rxjava3 = ["rxjava3-core", "rxjava3-android", "rxjava3-kotlin"]
|
||||
xmldiff = ["diffx", "xmlwriter"]
|
||||
|
|
Loading…
Reference in New Issue