diff --git a/app/src/main/java/app/pachli/BottomSheetActivity.kt b/app/src/main/java/app/pachli/BottomSheetActivity.kt index 6eeb61dff..53483621b 100644 --- a/app/src/main/java/app/pachli/BottomSheetActivity.kt +++ b/app/src/main/java/app/pachli/BottomSheetActivity.kt @@ -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?) { diff --git a/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt b/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt index 5802a0ded..1eb1b1c11 100644 --- a/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt +++ b/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt @@ -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) { diff --git a/app/src/main/java/app/pachli/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/app/pachli/components/instancemute/fragment/InstanceListFragment.kt index cc816e1b4..d5962511b 100644 --- a/app/src/main/java/app/pachli/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/app/pachli/components/instancemute/fragment/InstanceListFragment.kt @@ -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, linkHeader: String?) { @@ -158,6 +147,6 @@ class InstanceListFragment : } companion object { - private const val TAG = "InstanceList" // logging tag + private const val TAG = "InstanceList" } } diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationsViewModel.kt b/app/src/main/java/app/pachli/components/notifications/NotificationsViewModel.kt index 8c00158cd..118c7683f 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationsViewModel.kt @@ -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) { diff --git a/app/src/main/java/app/pachli/components/report/adapter/StatusesPagingSource.kt b/app/src/main/java/app/pachli/components/report/adapter/StatusesPagingSource.kt index d021c170f..6d4e4314f 100644 --- a/app/src/main/java/app/pachli/components/report/adapter/StatusesPagingSource.kt +++ b/app/src/main/java/app/pachli/components/report/adapter/StatusesPagingSource.kt @@ -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 { - return mastodonApi.accountStatusesObservable( + private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List? { + return mastodonApi.accountStatuses( accountId = accountId, maxId = maxId, sinceId = null, minId = minId, limit = limit, excludeReblogs = true, - ).await() + ).body() } } diff --git a/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusPagingSource.kt b/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusPagingSource.kt index 48c0ae0c2..048a754e3 100644 --- a/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusPagingSource.kt +++ b/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusPagingSource.kt @@ -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) } } } diff --git a/app/src/main/java/app/pachli/components/search/adapter/SearchPagingSource.kt b/app/src/main/java/app/pachli/components/search/adapter/SearchPagingSource.kt index 403da8042..b6929a13c 100644 --- a/app/src/main/java/app/pachli/components/search/adapter/SearchPagingSource.kt +++ b/app/src/main/java/app/pachli/components/search/adapter/SearchPagingSource.kt @@ -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( private val mastodonApi: MastodonApi, @@ -53,31 +53,27 @@ class SearchPagingSource( 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, + ) } } diff --git a/app/src/main/java/app/pachli/di/NetworkModule.kt b/app/src/main/java/app/pachli/di/NetworkModule.kt index 5eba7b8c0..a1bf2a309 100644 --- a/app/src/main/java/app/pachli/di/NetworkModule.kt +++ b/app/src/main/java/app/pachli/di/NetworkModule.kt @@ -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() } diff --git a/app/src/main/java/app/pachli/network/MastodonApi.kt b/app/src/main/java/app/pachli/network/MastodonApi.kt index 74e4ca7d3..a7cfe4642 100644 --- a/app/src/main/java/app/pachli/network/MastodonApi.kt +++ b/app/src/main/java/app/pachli/network/MastodonApi.kt @@ -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 @GET("api/v1/scheduled_statuses") - fun scheduledStatuses( + suspend fun scheduledStatuses( @Query("limit") limit: Int? = null, @Query("max_id") maxId: String? = null, - ): Single> + ): NetworkResult> @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> @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> @FormUrlEncoded @POST("api/v1/domain_blocks") @@ -509,14 +511,14 @@ interface MastodonApi { ): Response> @POST("api/v1/follow_requests/{id}/authorize") - fun authorizeFollowRequest( + suspend fun authorizeFollowRequest( @Path("id") accountId: String, - ): Single + ): NetworkResult @POST("api/v1/follow_requests/{id}/reject") - fun rejectFollowRequest( + suspend fun rejectFollowRequest( @Path("id") accountId: String, - ): Single + ): NetworkResult @FormUrlEncoded @POST("api/v1/apps") @@ -716,34 +718,19 @@ interface MastodonApi { @Field("forward") isNotifyRemote: Boolean?, ): NetworkResult - @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> - - @GET("api/v1/statuses/{id}") - fun statusObservable( - @Path("id") statusId: String, - ): Single - @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 + ): NetworkResult @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, diff --git a/app/src/main/java/app/pachli/usecase/TimelineCases.kt b/app/src/main/java/app/pachli/usecase/TimelineCases.kt index ceedca2af..92548e605 100644 --- a/app/src/main/java/app/pachli/usecase/TimelineCases.kt +++ b/app/src/main/java/app/pachli/usecase/TimelineCases.kt @@ -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 { + suspend fun acceptFollowRequest(accountId: String): NetworkResult { return mastodonApi.authorizeFollowRequest(accountId) } - fun rejectFollowRequest(accountId: String): Single { + suspend fun rejectFollowRequest(accountId: String): NetworkResult { return mastodonApi.rejectFollowRequest(accountId) } diff --git a/app/src/test/java/app/pachli/BottomSheetActivityTest.kt b/app/src/test/java/app/pachli/BottomSheetActivityTest.kt index 261f434cd..7fd9f9a0a 100644 --- a/app/src/test/java/app/pachli/BottomSheetActivityTest.kt +++ b/app/src/test/java/app/pachli/BottomSheetActivityTest.kt @@ -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 diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationAction.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationAction.kt index 9ce86c37d..6dced25a4 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationAction.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationAction.kt @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb09b0df7..523c180f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"]