introduce KotlinResultCallAdapter for nice suspending network calls (#2415)
* introduce KotlinResultCallAdapter for nice suspending network calls * fix tests
This commit is contained in:
parent
d21d045eda
commit
3e8c6a318a
15 changed files with 168 additions and 179 deletions
|
@ -137,6 +137,7 @@ dependencies {
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
||||||
|
implementation "at.connyduck:kotlin-result-calladapter:1.0.0"
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
||||||
|
@ -176,8 +177,8 @@ dependencies {
|
||||||
|
|
||||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||||
testImplementation "org.robolectric:robolectric:4.4"
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
testImplementation "org.mockito:mockito-inline:3.6.28"
|
testImplementation "org.mockito:mockito-inline:4.4.0"
|
||||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||||
|
|
|
@ -682,18 +682,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchUserInfo() {
|
private fun fetchUserInfo() = lifecycleScope.launch {
|
||||||
mastodonApi.accountVerifyCredentials()
|
mastodonApi.accountVerifyCredentials().fold(
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
{ userInfo ->
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
onFetchUserInfoSuccess(userInfo)
|
||||||
.subscribe(
|
},
|
||||||
{ userInfo ->
|
{ throwable ->
|
||||||
onFetchUserInfoSuccess(userInfo)
|
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||||
},
|
}
|
||||||
{ throwable ->
|
)
|
||||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchUserInfoSuccess(me: Account) {
|
private fun onFetchUserInfoSuccess(me: Account) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
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 kotlinx.coroutines.rx3.rxSingle
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AnnouncementsViewModel @Inject constructor(
|
class AnnouncementsViewModel @Inject constructor(
|
||||||
|
@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||||
.onErrorResumeNext {
|
.onErrorResumeNext {
|
||||||
mastodonApi.getInstance()
|
rxSingle {
|
||||||
.map { Either.Right(it) }
|
mastodonApi.getInstance().getOrThrow()
|
||||||
|
}.map { Either.Right(it) }
|
||||||
}
|
}
|
||||||
) { emojis, either ->
|
) { emojis, either ->
|
||||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||||
|
|
|
@ -48,6 +48,7 @@ import io.reactivex.rxjava3.core.Observable
|
||||||
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 kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.rx3.rxSingle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -105,7 +106,10 @@ class ComposeViewModel @Inject constructor(
|
||||||
init {
|
init {
|
||||||
|
|
||||||
Single.zip(
|
Single.zip(
|
||||||
api.getCustomEmojis(), api.getInstance()
|
api.getCustomEmojis(),
|
||||||
|
rxSingle {
|
||||||
|
api.getInstance().getOrThrow()
|
||||||
|
}
|
||||||
) { emojis, instance ->
|
) { emojis, instance ->
|
||||||
InstanceEntity(
|
InstanceEntity(
|
||||||
instance = accountManager.activeAccount?.domain!!,
|
instance = accountManager.activeAccount?.domain!!,
|
||||||
|
@ -291,7 +295,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
): LiveData<Unit> {
|
): LiveData<Unit> {
|
||||||
|
|
||||||
val deletionObservable = if (isEditingScheduledToot) {
|
val deletionObservable = if (isEditingScheduledToot) {
|
||||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
|
||||||
} else {
|
} else {
|
||||||
Observable.just(Unit)
|
Observable.just(Unit)
|
||||||
}.toLiveData()
|
}.toLiveData()
|
||||||
|
|
|
@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.AppCredentials
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
import com.keylesspalace.tusky.util.rickRoll
|
import com.keylesspalace.tusky.util.rickRoll
|
||||||
|
@ -166,32 +165,33 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val credentials: AppCredentials = try {
|
mastodonApi.authenticateApp(
|
||||||
mastodonApi.authenticateApp(
|
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
).fold(
|
||||||
)
|
{ credentials ->
|
||||||
} catch (e: Exception) {
|
// Before we open browser page we save the data.
|
||||||
binding.loginButton.isEnabled = true
|
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||||
binding.domainTextInputLayout.error =
|
// and we will need to pick up the process where we left off.
|
||||||
getString(R.string.error_failed_app_registration)
|
// Alternatively we could pass it all as part of the intent and receive it back
|
||||||
setLoading(false)
|
// but it is a bit of a workaround.
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
preferences.edit()
|
||||||
return@launch
|
.putString(DOMAIN, domain)
|
||||||
}
|
.putString(CLIENT_ID, credentials.clientId)
|
||||||
|
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||||
|
.apply()
|
||||||
|
|
||||||
// Before we open browser page we save the data.
|
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||||
// Even if we don't open other apps user may go to password manager or somewhere else
|
},
|
||||||
// and we will need to pick up the process where we left off.
|
{ e ->
|
||||||
// Alternatively we could pass it all as part of the intent and receive it back
|
binding.loginButton.isEnabled = true
|
||||||
// but it is a bit of a workaround.
|
binding.domainTextInputLayout.error =
|
||||||
preferences.edit()
|
getString(R.string.error_failed_app_registration)
|
||||||
.putString(DOMAIN, domain)
|
setLoading(false)
|
||||||
.putString(CLIENT_ID, credentials.clientId)
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
return@launch
|
||||||
.apply()
|
}
|
||||||
|
)
|
||||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,29 +224,28 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
val accessToken = try {
|
mastodonApi.fetchOAuthToken(
|
||||||
mastodonApi.fetchOAuthToken(
|
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||||
domain, clientId, clientSecret, oauthRedirectUri, code,
|
).fold(
|
||||||
"authorization_code"
|
{ accessToken ->
|
||||||
)
|
accountManager.addAccount(accessToken.accessToken, domain)
|
||||||
} catch (e: Exception) {
|
|
||||||
setLoading(false)
|
|
||||||
binding.domainTextInputLayout.error =
|
|
||||||
getString(R.string.error_retrieving_oauth_token)
|
|
||||||
Log.e(
|
|
||||||
TAG,
|
|
||||||
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accountManager.addAccount(accessToken.accessToken, domain)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
startActivity(intent)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
finish()
|
||||||
startActivity(intent)
|
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||||
finish()
|
},
|
||||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
{ e ->
|
||||||
|
setLoading(false)
|
||||||
|
binding.domainTextInputLayout.error =
|
||||||
|
getString(R.string.error_retrieving_oauth_token)
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLoading(loadingState: Boolean) {
|
private fun setLoading(loadingState: Boolean) {
|
||||||
|
|
|
@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ScheduledStatusViewModel @Inject constructor(
|
class ScheduledStatusViewModel @Inject constructor(
|
||||||
|
@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor(
|
||||||
|
|
||||||
fun deleteScheduledStatus(status: ScheduledStatus) {
|
fun deleteScheduledStatus(status: ScheduledStatus) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
mastodonApi.deleteScheduledStatus(status.id).fold(
|
||||||
mastodonApi.deleteScheduledStatus(status.id).await()
|
{
|
||||||
pagingSourceFactory.remove(status)
|
pagingSourceFactory.remove(status)
|
||||||
} catch (throwable: Throwable) {
|
},
|
||||||
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
{ throwable ->
|
||||||
}
|
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
@ -111,6 +112,7 @@ class NetworkModule {
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||||
|
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ interface MastodonApi {
|
||||||
fun getCustomEmojis(): Single<List<Emoji>>
|
fun getCustomEmojis(): Single<List<Emoji>>
|
||||||
|
|
||||||
@GET("api/v1/instance")
|
@GET("api/v1/instance")
|
||||||
fun getInstance(): Single<Instance>
|
suspend fun getInstance(): Result<Instance>
|
||||||
|
|
||||||
@GET("api/v1/filters")
|
@GET("api/v1/filters")
|
||||||
fun getFilters(): Single<List<Filter>>
|
fun getFilters(): Single<List<Filter>>
|
||||||
|
@ -249,12 +249,12 @@ interface MastodonApi {
|
||||||
): Single<List<ScheduledStatus>>
|
): Single<List<ScheduledStatus>>
|
||||||
|
|
||||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||||
fun deleteScheduledStatus(
|
suspend fun deleteScheduledStatus(
|
||||||
@Path("id") scheduledStatusId: String
|
@Path("id") scheduledStatusId: String
|
||||||
): Single<ResponseBody>
|
): Result<ResponseBody>
|
||||||
|
|
||||||
@GET("api/v1/accounts/verify_credentials")
|
@GET("api/v1/accounts/verify_credentials")
|
||||||
fun accountVerifyCredentials(): Single<Account>
|
suspend fun accountVerifyCredentials(): Result<Account>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PATCH("api/v1/accounts/update_credentials")
|
@PATCH("api/v1/accounts/update_credentials")
|
||||||
|
@ -265,7 +265,7 @@ interface MastodonApi {
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@PATCH("api/v1/accounts/update_credentials")
|
@PATCH("api/v1/accounts/update_credentials")
|
||||||
fun accountUpdateCredentials(
|
suspend fun accountUpdateCredentials(
|
||||||
@Part(value = "display_name") displayName: RequestBody?,
|
@Part(value = "display_name") displayName: RequestBody?,
|
||||||
@Part(value = "note") note: RequestBody?,
|
@Part(value = "note") note: RequestBody?,
|
||||||
@Part(value = "locked") locked: RequestBody?,
|
@Part(value = "locked") locked: RequestBody?,
|
||||||
|
@ -279,7 +279,7 @@ interface MastodonApi {
|
||||||
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
||||||
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
||||||
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
||||||
): Call<Account>
|
): Result<Account>
|
||||||
|
|
||||||
@GET("api/v1/accounts/search")
|
@GET("api/v1/accounts/search")
|
||||||
fun searchAccounts(
|
fun searchAccounts(
|
||||||
|
@ -447,7 +447,7 @@ interface MastodonApi {
|
||||||
@Field("redirect_uris") redirectUris: String,
|
@Field("redirect_uris") redirectUris: String,
|
||||||
@Field("scopes") scopes: String,
|
@Field("scopes") scopes: String,
|
||||||
@Field("website") website: String
|
@Field("website") website: String
|
||||||
): AppCredentials
|
): Result<AppCredentials>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("oauth/token")
|
@POST("oauth/token")
|
||||||
|
@ -458,7 +458,7 @@ interface MastodonApi {
|
||||||
@Field("redirect_uri") redirectUri: String,
|
@Field("redirect_uri") redirectUri: String,
|
||||||
@Field("code") code: String,
|
@Field("code") code: String,
|
||||||
@Field("grant_type") grantType: String
|
@Field("grant_type") grantType: String
|
||||||
): AccessToken
|
): Result<AccessToken>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/lists")
|
@POST("api/v1/lists")
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
@ -31,8 +32,7 @@ import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import kotlinx.coroutines.launch
|
||||||
import io.reactivex.rxjava3.kotlin.addTo
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import retrofit2.Call
|
import retrofit2.HttpException
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor(
|
||||||
|
|
||||||
private var oldProfileData: Account? = null
|
private var oldProfileData: Account? = null
|
||||||
|
|
||||||
private val disposables = CompositeDisposable()
|
fun obtainProfile() = viewModelScope.launch {
|
||||||
|
|
||||||
fun obtainProfile() {
|
|
||||||
if (profileData.value == null || profileData.value is Error) {
|
if (profileData.value == null || profileData.value is Error) {
|
||||||
|
|
||||||
profileData.postValue(Loading())
|
profileData.postValue(Loading())
|
||||||
|
|
||||||
mastodonApi.accountVerifyCredentials()
|
mastodonApi.accountVerifyCredentials().fold(
|
||||||
.subscribe(
|
{ profile ->
|
||||||
{ profile ->
|
oldProfileData = profile
|
||||||
oldProfileData = profile
|
profileData.postValue(Success(profile))
|
||||||
profileData.postValue(Success(profile))
|
},
|
||||||
},
|
{
|
||||||
{
|
profileData.postValue(Error())
|
||||||
profileData.postValue(Error())
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
.addTo(disposables)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mastodonApi.accountUpdateCredentials(
|
viewModelScope.launch {
|
||||||
displayName, note, locked, avatar, header,
|
mastodonApi.accountUpdateCredentials(
|
||||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
displayName, note, locked, avatar, header,
|
||||||
).enqueue(object : Callback<Account> {
|
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
).fold(
|
||||||
val newProfileData = response.body()
|
{ newProfileData ->
|
||||||
if (!response.isSuccessful || newProfileData == null) {
|
saveData.postValue(Success())
|
||||||
val errorResponse = response.errorBody()?.string()
|
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||||
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
},
|
||||||
try {
|
{ throwable ->
|
||||||
JSONObject(errorResponse).optString("error", null)
|
if (throwable is HttpException) {
|
||||||
} catch (e: JSONException) {
|
val errorResponse = throwable.response()?.errorBody()?.string()
|
||||||
|
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
||||||
|
try {
|
||||||
|
JSONObject(errorResponse).optString("error", "")
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
saveData.postValue(Error(errorMessage = errorMsg))
|
||||||
} else {
|
} else {
|
||||||
null
|
saveData.postValue(Error())
|
||||||
}
|
}
|
||||||
saveData.postValue(Error(errorMessage = errorMsg))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
saveData.postValue(Success())
|
)
|
||||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
|
||||||
saveData.postValue(Error())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache activity state for rotation change
|
// cache activity state for rotation change
|
||||||
|
@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor(
|
||||||
return File(application.cacheDir, filename)
|
return File(application.cacheDir, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
fun obtainInstance() = viewModelScope.launch {
|
||||||
disposables.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun obtainInstance() {
|
|
||||||
if (instanceData.value == null || instanceData.value is Error) {
|
if (instanceData.value == null || instanceData.value is Error) {
|
||||||
instanceData.postValue(Loading())
|
instanceData.postValue(Loading())
|
||||||
|
|
||||||
mastodonApi.getInstance().subscribe(
|
mastodonApi.getInstance().fold(
|
||||||
{ instance ->
|
{ instance ->
|
||||||
instanceData.postValue(Success(instance))
|
instanceData.postValue(Success(instance))
|
||||||
},
|
},
|
||||||
|
@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor(
|
||||||
instanceData.postValue(Error())
|
instanceData.postValue(Error())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addTo(disposables)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,15 +16,11 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.text.SpannedString
|
import android.text.SpannedString
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.keylesspalace.tusky.entity.SearchResult
|
import com.keylesspalace.tusky.entity.SearchResult
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.nhaarman.mockitokotlin2.doReturn
|
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
|
||||||
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
@ -39,8 +35,8 @@ import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
import org.junit.runners.Parameterized
|
||||||
import org.mockito.ArgumentMatchers.anyBoolean
|
import org.mockito.ArgumentMatchers.anyBoolean
|
||||||
import org.mockito.Mockito.eq
|
import org.mockito.Mockito.eq
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.kotlin.doReturn
|
||||||
import java.util.ArrayList
|
import org.mockito.kotlin.mock
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -306,7 +302,7 @@ class BottomSheetActivityTest {
|
||||||
init {
|
init {
|
||||||
mastodonApi = api
|
mastodonApi = api
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior<LinearLayout>
|
bottomSheet = mock()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openLink(url: String) {
|
override fun openLink(url: String) {
|
||||||
|
|
|
@ -24,8 +24,6 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
|
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
|
||||||
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
|
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
|
||||||
import com.keylesspalace.tusky.components.compose.MediaUploader
|
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
@ -37,18 +35,16 @@ import com.keylesspalace.tusky.entity.Instance
|
||||||
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
||||||
import com.keylesspalace.tusky.entity.StatusConfiguration
|
import com.keylesspalace.tusky.entity.StatusConfiguration
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
|
||||||
import com.nhaarman.mockitokotlin2.any
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.core.SingleObserver
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mockito.`when`
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
import org.robolectric.Robolectric
|
import org.robolectric.Robolectric
|
||||||
import org.robolectric.Shadows.shadowOf
|
import org.robolectric.Shadows.shadowOf
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
@ -94,44 +90,47 @@ class ComposeActivityTest {
|
||||||
val controller = Robolectric.buildActivity(ComposeActivity::class.java)
|
val controller = Robolectric.buildActivity(ComposeActivity::class.java)
|
||||||
activity = controller.get()
|
activity = controller.get()
|
||||||
|
|
||||||
accountManagerMock = mock(AccountManager::class.java)
|
accountManagerMock = mock {
|
||||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
on { activeAccount } doReturn account
|
||||||
|
}
|
||||||
|
|
||||||
apiMock = mock(MastodonApi::class.java)
|
apiMock = mock {
|
||||||
`when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList()))
|
on { getCustomEmojis() } doReturn Single.just(emptyList())
|
||||||
`when`(apiMock.getInstance()).thenReturn(object : Single<Instance>() {
|
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
|
||||||
override fun subscribeActual(observer: SingleObserver<in Instance>) {
|
|
||||||
val instance = instanceResponseCallback?.invoke()
|
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
observer.onError(Throwable())
|
Result.failure(Throwable())
|
||||||
} else {
|
} else {
|
||||||
observer.onSuccess(instance)
|
Result.success(instance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
val instanceDaoMock = mock(InstanceDao::class.java)
|
val instanceDaoMock: InstanceDao = mock {
|
||||||
`when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn(
|
on { loadMetadataForInstance(any()) } doReturn
|
||||||
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
|
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
|
||||||
)
|
on { loadMetadataForInstance(any()) } doReturn
|
||||||
|
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
|
||||||
|
}
|
||||||
|
|
||||||
val dbMock = mock(AppDatabase::class.java)
|
val dbMock: AppDatabase = mock {
|
||||||
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
|
on { instanceDao() } doReturn instanceDaoMock
|
||||||
|
}
|
||||||
|
|
||||||
val viewModel = ComposeViewModel(
|
val viewModel = ComposeViewModel(
|
||||||
apiMock,
|
apiMock,
|
||||||
accountManagerMock,
|
accountManagerMock,
|
||||||
mock(MediaUploader::class.java),
|
mock(),
|
||||||
mock(ServiceClient::class.java),
|
mock(),
|
||||||
mock(DraftHelper::class.java),
|
mock(),
|
||||||
dbMock
|
dbMock
|
||||||
)
|
)
|
||||||
activity.intent = Intent(activity, ComposeActivity::class.java).apply {
|
activity.intent = Intent(activity, ComposeActivity::class.java).apply {
|
||||||
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
|
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewModelFactoryMock = mock(ViewModelFactory::class.java)
|
val viewModelFactoryMock: ViewModelFactory = mock {
|
||||||
`when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel)
|
on { create(ComposeViewModel::class.java) } doReturn viewModel
|
||||||
|
}
|
||||||
|
|
||||||
activity.accountManager = accountManagerMock
|
activity.accountManager = accountManagerMock
|
||||||
activity.viewModelFactory = viewModelFactoryMock
|
activity.viewModelFactory = viewModelFactoryMock
|
||||||
|
@ -490,7 +489,7 @@ class ComposeActivityTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
|
private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
|
||||||
return InstanceConfiguration(
|
return InstanceConfiguration(
|
||||||
statuses = StatusConfiguration(
|
statuses = StatusConfiguration(
|
||||||
maxCharacters = maximumStatusCharacters,
|
maxCharacters = maximumStatusCharacters,
|
||||||
|
|
|
@ -8,12 +8,12 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.PollOption
|
import com.keylesspalace.tusky.entity.PollOption
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -22,7 +22,7 @@ import java.util.Date
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class FilterTest {
|
class FilterTest {
|
||||||
|
|
||||||
lateinit var filterModel: FilterModel
|
private lateinit var filterModel: FilterModel
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
|
|
@ -17,9 +17,6 @@ import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.Converters
|
import com.keylesspalace.tusky.db.Converters
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.nhaarman.mockitokotlin2.anyOrNull
|
|
||||||
import com.nhaarman.mockitokotlin2.doReturn
|
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -31,6 +28,9 @@ import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.anyOrNull
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
import org.robolectric.Shadows.shadowOf
|
import org.robolectric.Shadows.shadowOf
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
|
@ -3,11 +3,11 @@ package com.keylesspalace.tusky.components.timeline
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
|
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||||
import com.nhaarman.mockitokotlin2.doReturn
|
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
|
||||||
class NetworkTimelinePagingSourceTest {
|
class NetworkTimelinePagingSourceTest {
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,6 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineView
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import com.nhaarman.mockitokotlin2.anyOrNull
|
|
||||||
import com.nhaarman.mockitokotlin2.doReturn
|
|
||||||
import com.nhaarman.mockitokotlin2.doThrow
|
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
|
||||||
import com.nhaarman.mockitokotlin2.verify
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
@ -24,6 +19,11 @@ import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.anyOrNull
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.doThrow
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
@ -331,7 +331,6 @@ class NetworkTimelineRemoteMediatorTest {
|
||||||
mockStatusViewData("2"),
|
mockStatusViewData("2"),
|
||||||
mockStatusViewData("1"),
|
mockStatusViewData("1"),
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(timelineViewModel).nextKey = "0"
|
verify(timelineViewModel).nextKey = "0"
|
||||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||||
|
|
Loading…
Reference in a new issue