From 91d577c12818f6cbe062bafb73cc233b9b046890 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Wed, 5 Feb 2025 13:37:59 +0100 Subject: [PATCH] refactor: Replace the different network response types with ApiResult (#1261) Previous code used five (!) different types for the network response. Some used Retrofit's `Response`. This provides access to the headers. Some used `NetworkResult`. This did not provide access to the headers, but did provide some higher-order functions (e.g., `fold`) for operating on the results. One used a raw `Map`. One used a raw `Call`. The rest had been converted to `ApiResult`, a `Result`. This provides the higher-order functions, provides the headers, and is exception-free, so is the correct type to use. This PR completes the work of cutting over to `ApiResult`. The return values are changed and the calling code is adjusted to use the new functions as appropriate. --- .../kotlin/app/pachli/di/UpdateCheckModule.kt | 4 +- .../app/pachli/updatecheck/FdroidService.kt | 4 +- .../app/pachli/updatecheck/UpdateCheck.kt | 3 +- .../kotlin/app/pachli/di/UpdateCheckModule.kt | 4 +- .../app/pachli/updatecheck/GithubService.kt | 4 +- .../app/pachli/updatecheck/UpdateCheck.kt | 3 +- .../main/java/app/pachli/TimelineActivity.kt | 63 +++--- .../components/account/AccountViewModel.kt | 16 +- .../media/AccountMediaRemoteMediator.kt | 10 +- .../accountlist/AccountListFragment.kt | 20 +- .../announcements/AnnouncementsViewModel.kt | 143 +++++++------- .../components/compose/ComposeViewModel.kt | 7 +- .../ConversationsRemoteMediator.kt | 10 +- .../conversation/ConversationsViewModel.kt | 26 +-- .../components/drafts/DraftsActivity.kt | 81 ++++---- .../components/drafts/DraftsViewModel.kt | 6 +- .../followedtags/FollowedTagsActivity.kt | 91 ++++----- .../FollowedTagsRemoteMediator.kt | 16 +- .../followedtags/FollowedTagsViewModel.kt | 7 +- .../fragment/InstanceListFragment.kt | 17 +- .../notifications/NotificationFetcher.kt | 30 +-- .../notifications/NotificationsFragment.kt | 5 +- .../notifications/NotificationsViewModel.kt | 74 +++---- .../preference/AccountPreferencesFragment.kt | 51 ++--- .../components/report/ReportViewModel.kt | 24 +-- .../report/adapter/StatusesPagingSource.kt | 5 +- .../scheduled/ScheduledStatusPagingSource.kt | 14 +- .../scheduled/ScheduledStatusViewModel.kt | 14 +- .../components/search/SearchViewModel.kt | 45 ++--- .../search/adapter/SearchPagingSource.kt | 8 +- .../fragments/SearchStatusesFragment.kt | 111 +++++------ .../timeline/CachedTimelineRepository.kt | 50 ++--- .../components/timeline/TimelineFragment.kt | 4 +- .../components/timeline/util/TimelineUtils.kt | 17 -- .../viewmodel/CachedTimelineRemoteMediator.kt | 26 +-- .../NetworkTimelineRemoteMediator.kt | 12 +- .../timeline/viewmodel/PageCache.kt | 33 ++-- .../timeline/viewmodel/TimelineViewModel.kt | 101 +++++----- .../viewmodel/TrendingLinksViewModel.kt | 8 +- .../viewmodel/TrendingTagsViewModel.kt | 19 +- .../viewthread/ViewThreadViewModel.kt | 185 ++++++++---------- .../viewthread/edits/ViewEditsViewModel.kt | 6 +- .../java/app/pachli/fragment/SFragment.kt | 100 +++++----- .../app/pachli/service/SendStatusService.kt | 47 +++-- .../java/app/pachli/usecase/TimelineCases.kt | 45 ++--- .../pachli/viewmodel/EditProfileViewModel.kt | 19 +- .../components/compose/ComposeActivityTest.kt | 3 +- .../NotificationsViewModelTestBase.kt | 13 -- ...icationsViewModelTestClearNotifications.kt | 6 +- ...nsViewModelTestNotificationFilterAction.kt | 12 +- ...icationsViewModelTestStatusFilterAction.kt | 13 +- .../CachedTimelineRemoteMediatorTest.kt | 24 +-- .../CachedTimelineViewModelTestBase.kt | 10 - ...TimelineViewModelTestStatusFilterAction.kt | 20 +- .../NetworkTimelineRemoteMediatorTest.kt | 19 +- .../NetworkTimelineViewModelTestBase.kt | 10 - ...TimelineViewModelTestStatusFilterAction.kt | 20 +- .../viewthread/ViewThreadViewModelTest.kt | 18 +- .../app/pachli/usecase/TimelineCasesTest.kt | 28 +-- .../app/pachli/updatecheck/UpdateCheckTest.kt | 9 +- .../core/activity/BottomSheetActivity.kt | 48 +++-- .../core/activity/BottomSheetActivityTest.kt | 8 +- .../NotificationsRemoteMediator.kt | 27 ++- .../notifications/NotificationsRepository.kt | 35 ++-- core/network-test/build.gradle.kts | 3 + core/network/build.gradle.kts | 1 - .../pachli/core/network/di/NetworkModule.kt | 2 - .../core/network/retrofit/MastodonApi.kt | 121 ++++++------ core/testing/build.gradle.kts | 4 + core/ui/build.gradle.kts | 4 +- .../app/pachli/feature/login/LoginActivity.kt | 89 ++++----- .../login/src/main/res/values-ar/strings.xml | 2 +- .../login/src/main/res/values-be/strings.xml | 2 +- .../login/src/main/res/values-bg/strings.xml | 2 +- .../src/main/res/values-bn-rBD/strings.xml | 2 +- .../src/main/res/values-bn-rIN/strings.xml | 16 +- .../login/src/main/res/values-ca/strings.xml | 2 +- .../login/src/main/res/values-ckb/strings.xml | 16 +- .../login/src/main/res/values-cs/strings.xml | 2 +- .../login/src/main/res/values-cy/strings.xml | 2 +- .../login/src/main/res/values-de/strings.xml | 2 +- .../src/main/res/values-en-rGB/strings.xml | 2 +- .../login/src/main/res/values-eo/strings.xml | 2 +- .../login/src/main/res/values-es/strings.xml | 2 +- .../login/src/main/res/values-eu/strings.xml | 2 +- .../login/src/main/res/values-fa/strings.xml | 2 +- .../login/src/main/res/values-fi/strings.xml | 2 +- .../login/src/main/res/values-fr/strings.xml | 2 +- .../login/src/main/res/values-fy/strings.xml | 2 +- .../login/src/main/res/values-ga/strings.xml | 2 +- .../login/src/main/res/values-gd/strings.xml | 2 +- .../login/src/main/res/values-gl/strings.xml | 2 +- .../login/src/main/res/values-hi/strings.xml | 2 +- .../login/src/main/res/values-hu/strings.xml | 2 +- .../login/src/main/res/values-in/strings.xml | 2 +- .../login/src/main/res/values-is/strings.xml | 2 +- .../login/src/main/res/values-it/strings.xml | 2 +- .../login/src/main/res/values-ja/strings.xml | 2 +- .../login/src/main/res/values-ko/strings.xml | 16 +- .../login/src/main/res/values-lv/strings.xml | 2 +- .../login/src/main/res/values-ml/strings.xml | 2 +- .../src/main/res/values-nb-rNO/strings.xml | 2 +- .../login/src/main/res/values-nl/strings.xml | 2 +- .../login/src/main/res/values-oc/strings.xml | 2 +- .../login/src/main/res/values-pl/strings.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 2 +- .../src/main/res/values-pt-rPT/strings.xml | 2 +- .../login/src/main/res/values-ru/strings.xml | 16 +- .../login/src/main/res/values-sa/strings.xml | 2 +- .../login/src/main/res/values-sk/strings.xml | 2 +- .../login/src/main/res/values-sl/strings.xml | 16 +- .../login/src/main/res/values-sv/strings.xml | 2 +- .../login/src/main/res/values-ta/strings.xml | 8 +- .../login/src/main/res/values-th/strings.xml | 16 +- .../login/src/main/res/values-tr/strings.xml | 2 +- .../login/src/main/res/values-uk/strings.xml | 2 +- .../login/src/main/res/values-vi/strings.xml | 2 +- .../src/main/res/values-zh-rCN/strings.xml | 2 +- .../src/main/res/values-zh-rHK/strings.xml | 4 +- .../src/main/res/values-zh-rMO/strings.xml | 4 +- .../src/main/res/values-zh-rSG/strings.xml | 12 +- .../src/main/res/values-zh-rTW/strings.xml | 2 +- feature/login/src/main/res/values/strings.xml | 2 +- gradle/libs.versions.toml | 2 - 124 files changed, 1063 insertions(+), 1282 deletions(-) delete mode 100644 app/src/main/java/app/pachli/components/timeline/util/TimelineUtils.kt diff --git a/app/src/fdroid/kotlin/app/pachli/di/UpdateCheckModule.kt b/app/src/fdroid/kotlin/app/pachli/di/UpdateCheckModule.kt index 1988bf5831..08f9171c0c 100644 --- a/app/src/fdroid/kotlin/app/pachli/di/UpdateCheckModule.kt +++ b/app/src/fdroid/kotlin/app/pachli/di/UpdateCheckModule.kt @@ -17,8 +17,8 @@ package app.pachli.di +import app.pachli.core.network.retrofit.apiresult.ApiResultCallAdapterFactory import app.pachli.updatecheck.FdroidService -import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -40,7 +40,7 @@ object UpdateCheckModule { .baseUrl("https://f-droid.org") .client(httpClient) .addConverterFactory(MoshiConverterFactory.create()) - .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) + .addCallAdapterFactory(ApiResultCallAdapterFactory.create()) .build() .create() } diff --git a/app/src/fdroid/kotlin/app/pachli/updatecheck/FdroidService.kt b/app/src/fdroid/kotlin/app/pachli/updatecheck/FdroidService.kt index 5a4110c088..b3c713177e 100644 --- a/app/src/fdroid/kotlin/app/pachli/updatecheck/FdroidService.kt +++ b/app/src/fdroid/kotlin/app/pachli/updatecheck/FdroidService.kt @@ -18,7 +18,7 @@ package app.pachli.updatecheck import androidx.annotation.Keep -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.network.retrofit.apiresult.ApiResult import com.squareup.moshi.JsonClass import retrofit2.http.GET import retrofit2.http.Path @@ -42,5 +42,5 @@ interface FdroidService { @GET("/api/v1/packages/{package}") suspend fun getPackage( @Path("package") pkg: String, - ): NetworkResult + ): ApiResult } diff --git a/app/src/fdroid/kotlin/app/pachli/updatecheck/UpdateCheck.kt b/app/src/fdroid/kotlin/app/pachli/updatecheck/UpdateCheck.kt index bd099aaa7e..a5b5e7451a 100644 --- a/app/src/fdroid/kotlin/app/pachli/updatecheck/UpdateCheck.kt +++ b/app/src/fdroid/kotlin/app/pachli/updatecheck/UpdateCheck.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.net.Uri import app.pachli.BuildConfig import app.pachli.core.preferences.SharedPreferencesRepository +import com.github.michaelbull.result.get import javax.inject.Inject import javax.inject.Singleton @@ -34,7 +35,7 @@ class UpdateCheck @Inject constructor( } override suspend fun remoteFetchLatestVersionCode(): Int? { - val fdroidPackage = fdroidService.getPackage(BuildConfig.APPLICATION_ID).getOrNull() ?: return null + val fdroidPackage = fdroidService.getPackage(BuildConfig.APPLICATION_ID).get()?.body ?: return null // `packages` is a list of all packages that have been built and are available. // diff --git a/app/src/github/kotlin/app/pachli/di/UpdateCheckModule.kt b/app/src/github/kotlin/app/pachli/di/UpdateCheckModule.kt index 88aa167f60..a1a0aee037 100644 --- a/app/src/github/kotlin/app/pachli/di/UpdateCheckModule.kt +++ b/app/src/github/kotlin/app/pachli/di/UpdateCheckModule.kt @@ -17,8 +17,8 @@ package app.pachli.di +import app.pachli.core.network.retrofit.apiresult.ApiResultCallAdapterFactory import app.pachli.updatecheck.GitHubService -import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -40,7 +40,7 @@ object UpdateCheckModule { .baseUrl("https://api.github.com") .client(httpClient) .addConverterFactory(MoshiConverterFactory.create()) - .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) + .addCallAdapterFactory(ApiResultCallAdapterFactory.create()) .build() .create() } diff --git a/app/src/github/kotlin/app/pachli/updatecheck/GithubService.kt b/app/src/github/kotlin/app/pachli/updatecheck/GithubService.kt index 6643e5064d..c56bb35fae 100644 --- a/app/src/github/kotlin/app/pachli/updatecheck/GithubService.kt +++ b/app/src/github/kotlin/app/pachli/updatecheck/GithubService.kt @@ -18,7 +18,7 @@ package app.pachli.updatecheck import androidx.annotation.Keep -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.network.retrofit.apiresult.ApiResult import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import retrofit2.http.GET @@ -47,5 +47,5 @@ interface GitHubService { suspend fun getLatestRelease( @Path("owner") owner: String, @Path("repo") repo: String, - ): NetworkResult + ): ApiResult } diff --git a/app/src/github/kotlin/app/pachli/updatecheck/UpdateCheck.kt b/app/src/github/kotlin/app/pachli/updatecheck/UpdateCheck.kt index 7c5ba3ecde..962e6ed209 100644 --- a/app/src/github/kotlin/app/pachli/updatecheck/UpdateCheck.kt +++ b/app/src/github/kotlin/app/pachli/updatecheck/UpdateCheck.kt @@ -20,6 +20,7 @@ package app.pachli.updatecheck import android.content.Intent import android.net.Uri import app.pachli.core.preferences.SharedPreferencesRepository +import com.github.michaelbull.result.get import javax.inject.Inject class UpdateCheck @Inject constructor( @@ -33,7 +34,7 @@ class UpdateCheck @Inject constructor( } override suspend fun remoteFetchLatestVersionCode(): Int? { - val release = gitHubService.getLatestRelease("pachli", "pachli-android").getOrNull() ?: return null + val release = gitHubService.getLatestRelease("pachli", "pachli-android").get()?.body ?: return null for (asset in release.assets) { if (asset.contentType != "application/vnd.android.package-archive") continue return versionCodeExtractor.find(asset.name)?.groups?.get(1)?.value?.toIntOrNull() ?: continue diff --git a/app/src/main/java/app/pachli/TimelineActivity.kt b/app/src/main/java/app/pachli/TimelineActivity.kt index aef8a0b727..b80c8b0807 100644 --- a/app/src/main/java/app/pachli/TimelineActivity.kt +++ b/app/src/main/java/app/pachli/TimelineActivity.kt @@ -44,7 +44,6 @@ import app.pachli.core.navigation.pachliAccountId import app.pachli.databinding.ActivityTimelineBinding import app.pachli.interfaces.ActionButtonActivity import app.pachli.interfaces.AppBarLayoutHost -import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import com.google.android.material.appbar.AppBarLayout @@ -135,21 +134,19 @@ class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonAc hashtag?.let { tag -> lifecycleScope.launch { - mastodonApi.tag(tag).fold( - { tagEntity -> - menuInflater.inflate(R.menu.view_hashtag_toolbar, menu) - followTagItem = menu.findItem(R.id.action_follow_hashtag) - unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag) - muteTagItem = menu.findItem(R.id.action_mute_hashtag) - unmuteTagItem = menu.findItem(R.id.action_unmute_hashtag) - followTagItem?.isVisible = tagEntity.following == false - unfollowTagItem?.isVisible = tagEntity.following == true - updateMuteTagMenuItems(tag) - }, - { - Timber.w(it, "Failed to query tag #%s", tag) - }, - ) + mastodonApi.tag(tag).onSuccess { + val tagEntity = it.body + menuInflater.inflate(R.menu.view_hashtag_toolbar, menu) + followTagItem = menu.findItem(R.id.action_follow_hashtag) + unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag) + muteTagItem = menu.findItem(R.id.action_mute_hashtag) + unmuteTagItem = menu.findItem(R.id.action_unmute_hashtag) + followTagItem?.isVisible = tagEntity.following == false + unfollowTagItem?.isVisible = tagEntity.following == true + updateMuteTagMenuItems(tag) + }.onFailure { + Timber.w("Failed to query tag #%s: %s", tag, it) + } } } @@ -205,16 +202,13 @@ class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonAc val tag = hashtag if (tag != null) { lifecycleScope.launch { - mastodonApi.followTag(tag).fold( - { - followTagItem?.isVisible = false - unfollowTagItem?.isVisible = true - }, - { - Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show() - Timber.e(it, "Failed to follow #%s", tag) - }, - ) + mastodonApi.followTag(tag).onSuccess { + followTagItem?.isVisible = false + unfollowTagItem?.isVisible = true + }.onFailure { + Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show() + Timber.e("Failed to follow #%s: %s", tag, it) + } } } @@ -225,16 +219,13 @@ class TimelineActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButtonAc val tag = hashtag if (tag != null) { lifecycleScope.launch { - mastodonApi.unfollowTag(tag).fold( - { - followTagItem?.isVisible = true - unfollowTagItem?.isVisible = false - }, - { - Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show() - Timber.e(it, "Failed to unfollow #%s", tag) - }, - ) + mastodonApi.unfollowTag(tag).onSuccess { + followTagItem?.isVisible = true + unfollowTagItem?.isVisible = false + }.onFailure { + Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show() + Timber.e("Failed to unfollow #%s: %s", tag, it) + } } } diff --git a/app/src/main/java/app/pachli/components/account/AccountViewModel.kt b/app/src/main/java/app/pachli/components/account/AccountViewModel.kt index f25cd0a840..a67ad8d52d 100644 --- a/app/src/main/java/app/pachli/components/account/AccountViewModel.kt +++ b/app/src/main/java/app/pachli/components/account/AccountViewModel.kt @@ -84,9 +84,9 @@ class AccountViewModel @AssistedInject constructor( isFromOwnDomain = domain == activeAccount.domain } - .onFailure { t -> - Timber.w("failed obtaining account: %s", t) - accountData.postValue(Error(cause = t.throwable)) + .onFailure { + Timber.w("failed obtaining account: %s", it) + accountData.postValue(Error(cause = it.throwable)) isDataLoading = false isRefreshing.postValue(false) } @@ -100,13 +100,13 @@ class AccountViewModel @AssistedInject constructor( viewModelScope.launch { mastodonApi.relationships(listOf(accountId)) - .onSuccess { response -> - val relationships = response.body + .onSuccess { + val relationships = it.body relationshipData.postValue(if (relationships.isNotEmpty()) Success(relationships[0]) else Error()) } - .onFailure { t -> - Timber.w("failed obtaining relationships: %s", t) - relationshipData.postValue(Error(cause = t.throwable)) + .onFailure { + Timber.w("failed obtaining relationships: %s", it) + relationshipData.postValue(Error(cause = it.throwable)) } } } diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt index 27371d0eff..59d703e7d4 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt @@ -23,9 +23,9 @@ import androidx.paging.RemoteMediator import app.pachli.core.database.model.AccountEntity import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.network.retrofit.MastodonApi +import com.github.michaelbull.result.getOrElse import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive -import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) class AccountMediaRemoteMediator( @@ -53,13 +53,9 @@ class AccountMediaRemoteMediator( return MediatorResult.Success(endOfPaginationReached = false) } } - } - - val statuses = statusResponse.body() - if (!statusResponse.isSuccessful || statuses == null) { - return MediatorResult.Error(HttpException(statusResponse)) - } + }.getOrElse { return MediatorResult.Error(it.throwable) } + val statuses = statusResponse.body val attachments = statuses.flatMap { status -> AttachmentViewData.list(status, activeAccount.alwaysShowSensitiveMedia) } 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 31337a4756..4c5fcbb6f7 100644 --- a/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt +++ b/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt @@ -61,7 +61,6 @@ import app.pachli.databinding.FragmentAccountListBinding import app.pachli.interfaces.AccountActionListener import app.pachli.interfaces.AppBarLayoutHost import app.pachli.view.EndlessOnScrollListener -import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess @@ -273,19 +272,12 @@ class AccountListFragment : api.authorizeFollowRequest(accountId) } else { api.rejectFollowRequest(accountId) - }.fold( - { - onRespondToFollowRequestSuccess(position) - }, - { throwable -> - val verb = if (accept) { - "accept" - } else { - "reject" - } - Timber.e(throwable, "Failed to %s accountId %s", verb, accountId) - }, - ) + }.onSuccess { + onRespondToFollowRequestSuccess(position) + }.onFailure { error -> + val verb = if (accept) "accept" else "reject" + Timber.e("Failed to %s accountId %s: %s", verb, accountId, error.fmt(requireContext())) + } } } diff --git a/app/src/main/java/app/pachli/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/app/pachli/components/announcements/AnnouncementsViewModel.kt index 76d56f71cf..1fe81a5b69 100644 --- a/app/src/main/java/app/pachli/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/app/pachli/components/announcements/AnnouncementsViewModel.kt @@ -29,7 +29,6 @@ import app.pachli.util.Error import app.pachli.util.Loading import app.pachli.util.Resource import app.pachli.util.Success -import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel @@ -80,89 +79,83 @@ class AnnouncementsViewModel @Inject constructor( fun addReaction(announcementId: String, name: String) { viewModelScope.launch { mastodonApi.addAnnouncementReaction(announcementId, name) - .fold( - { - announcementsMutable.postValue( - Success( - announcements.value?.data?.map { announcement -> - if (announcement.id == announcementId) { - announcement.copy( - reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { - announcement.reactions.map { reaction -> - if (reaction.name == name) { - reaction.copy( - count = reaction.count + 1, - me = true, - ) - } else { - reaction - } + .onSuccess { + announcementsMutable.postValue( + Success( + announcements.value?.data?.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { + announcement.reactions.map { reaction -> + if (reaction.name == name) { + reaction.copy( + count = reaction.count + 1, + me = true, + ) + } else { + reaction } - } else { - listOf( - *announcement.reactions.toTypedArray(), - emojis.value!!.find { emoji -> emoji.shortcode == name }!!.run { - Announcement.Reaction( - name, - 1, - true, - url, - staticUrl, - ) - }, - ) - }, - ) - } else { - announcement - } - }, - ), - ) - }, - { - Timber.w(it, "Failed to add reaction to the announcement.") - }, - ) + } + } else { + listOf( + *announcement.reactions.toTypedArray(), + emojis.value!!.find { emoji -> emoji.shortcode == name }!!.run { + Announcement.Reaction( + name, + 1, + true, + url, + staticUrl, + ) + }, + ) + }, + ) + } else { + announcement + } + }, + ), + ) + }.onFailure { + Timber.w("Failed to add reaction to the announcement: %s", it) + } } } fun removeReaction(announcementId: String, name: String) { viewModelScope.launch { mastodonApi.removeAnnouncementReaction(announcementId, name) - .fold( - { - announcementsMutable.postValue( - Success( - announcements.value!!.data!!.map { announcement -> - if (announcement.id == announcementId) { - announcement.copy( - reactions = announcement.reactions.mapNotNull { reaction -> - if (reaction.name == name) { - if (reaction.count > 1) { - reaction.copy( - count = reaction.count - 1, - me = false, - ) - } else { - null - } + .onSuccess { + announcementsMutable.postValue( + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = announcement.reactions.mapNotNull { reaction -> + if (reaction.name == name) { + if (reaction.count > 1) { + reaction.copy( + count = reaction.count - 1, + me = false, + ) } else { - reaction + null } - }, - ) - } else { - announcement - } - }, - ), - ) - }, - { - Timber.w(it, "Failed to remove reaction from the announcement.") - }, - ) + } else { + reaction + } + }, + ) + } else { + announcement + } + }, + ), + ) + }.onFailure { + Timber.w("Failed to remove reaction from the announcement: %s", it) + } } } } diff --git a/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt b/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt index 63827e4480..38e31f5cf1 100644 --- a/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt @@ -52,7 +52,6 @@ import app.pachli.core.ui.MentionSpan import app.pachli.service.MediaToSend import app.pachli.service.ServiceClient import app.pachli.service.StatusToSend -import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result @@ -550,15 +549,15 @@ class ComposeViewModel @AssistedInject constructor( } '#' -> { return api.search(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) - .fold({ searchResult -> - searchResult.hashtags.map { + .mapBoth({ response -> + response.body.hashtags.map { AutocompleteResult.HashtagResult( hashtag = it.name, usage7d = it.history.sumOf { it.uses }, ) }.sortedByDescending { it.usage7d } }, { e -> - Timber.e(e, "Autocomplete search for %s failed.", token) + Timber.e("Autocomplete search for %s failed: %s", token, e) emptyList() }) } diff --git a/app/src/main/java/app/pachli/components/conversation/ConversationsRemoteMediator.kt b/app/src/main/java/app/pachli/components/conversation/ConversationsRemoteMediator.kt index 823fe18a9c..95c2367ae7 100644 --- a/app/src/main/java/app/pachli/components/conversation/ConversationsRemoteMediator.kt +++ b/app/src/main/java/app/pachli/components/conversation/ConversationsRemoteMediator.kt @@ -10,9 +10,9 @@ import app.pachli.core.database.di.TransactionProvider import app.pachli.core.database.model.ConversationEntity import app.pachli.core.network.model.HttpHeaderLink import app.pachli.core.network.retrofit.MastodonApi +import com.github.michaelbull.result.getOrElse import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive -import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) class ConversationsRemoteMediator( @@ -43,18 +43,16 @@ class ConversationsRemoteMediator( try { val conversationsResponse = api.getConversations(maxId = nextKey, limit = state.config.pageSize) + .getOrElse { return MediatorResult.Error(it.throwable) } - val conversations = conversationsResponse.body() - if (!conversationsResponse.isSuccessful || conversations == null) { - return MediatorResult.Error(HttpException(conversationsResponse)) - } + val conversations = conversationsResponse.body transactionProvider { if (loadType == LoadType.REFRESH) { conversationsDao.deleteForAccount(activeAccount.id) } - val linkHeader = conversationsResponse.headers()["Link"] + val linkHeader = conversationsResponse.headers["Link"] val links = HttpHeaderLink.parse(linkHeader) nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") diff --git a/app/src/main/java/app/pachli/components/conversation/ConversationsViewModel.kt b/app/src/main/java/app/pachli/components/conversation/ConversationsViewModel.kt index ac7d272eeb..988d82c92f 100644 --- a/app/src/main/java/app/pachli/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/app/pachli/components/conversation/ConversationsViewModel.kt @@ -33,7 +33,8 @@ import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.SharedPreferencesRepository import app.pachli.usecase.TimelineCases -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.currentCoroutineContext @@ -96,15 +97,15 @@ class ConversationsViewModel @Inject constructor( */ fun favourite(favourite: Boolean, lastStatusId: String) { viewModelScope.launch { - timelineCases.favourite(lastStatusId, favourite).fold({ + timelineCases.favourite(lastStatusId, favourite).onSuccess { conversationsDao.setFavourited( accountManager.activeAccount!!.id, lastStatusId, favourite, ) - }, { e -> - Timber.w(e, "failed to favourite status") - }) + }.onFailure { e -> + Timber.w("failed to favourite status: %s", e) + } } } @@ -113,15 +114,15 @@ class ConversationsViewModel @Inject constructor( */ fun bookmark(bookmark: Boolean, lastStatusId: String) { viewModelScope.launch { - timelineCases.bookmark(lastStatusId, bookmark).fold({ + timelineCases.bookmark(lastStatusId, bookmark).onSuccess { conversationsDao.setBookmarked( accountManager.activeAccount!!.id, lastStatusId, bookmark, ) - }, { e -> - Timber.w(e, "failed to bookmark status") - }) + }.onFailure { e -> + Timber.w("failed to bookmark status: %s", e) + } } } @@ -131,15 +132,14 @@ class ConversationsViewModel @Inject constructor( fun voteInPoll(choices: List, lastStatusId: String, pollId: String) { viewModelScope.launch { timelineCases.voteInPoll(lastStatusId, pollId, choices) - .fold({ poll -> + .onSuccess { + val poll = it.body conversationsDao.setVoted( accountManager.activeAccount!!.id, lastStatusId, converters.pollToJson(poll)!!, ) - }, { e -> - Timber.w(e, "failed to vote in poll") - }) + }.onFailure { Timber.w("failed to vote in poll: %s", it) } } } diff --git a/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt b/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt index 99009486ba..bf5a8881ae 100644 --- a/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt @@ -31,10 +31,12 @@ import app.pachli.core.navigation.ComposeActivityIntent import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.navigation.pachliAccountId import app.pachli.core.network.parseAsMastodonHtml +import app.pachli.core.network.retrofit.apiresult.ClientError import app.pachli.core.ui.BackgroundMessage import app.pachli.databinding.ActivityDraftsBinding import app.pachli.db.DraftsAlert -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.snackbar.Snackbar @@ -42,7 +44,6 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import retrofit2.HttpException import timber.log.Timber @AndroidEntryPoint @@ -106,45 +107,43 @@ class DraftsActivity : BaseActivity(), DraftActionListener { lifecycleScope.launch { bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED viewModel.getStatus(draft.inReplyToId!!) - .fold( - { status -> - val composeOptions = ComposeOptions( - draftId = draft.id, - content = draft.content, - contentWarning = draft.contentWarning, - inReplyToId = draft.inReplyToId, - replyingStatusContent = status.content.parseAsMastodonHtml().toString(), - replyingStatusAuthor = status.account.localUsername, - draftAttachments = draft.attachments, - poll = draft.poll, - sensitive = draft.sensitive, - visibility = draft.visibility, - scheduledAt = draft.scheduledAt, - language = draft.language, - statusId = draft.statusId, - kind = ComposeOptions.ComposeKind.EDIT_DRAFT, - ) - - bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN - - startActivity(ComposeActivityIntent(context, intent.pachliAccountId, composeOptions)) - }, - { throwable -> - bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN - - Timber.w(throwable, "failed loading reply information") - - if (throwable is HttpException && throwable.code() == 404) { - // the original status to which a reply was drafted has been deleted - // let's open the ComposeActivity without reply information - Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show() - openDraftWithoutReply(draft) - } else { - Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) - .show() - } - }, - ) + .onSuccess { + val status = it.body + val composeOptions = ComposeOptions( + draftId = draft.id, + content = draft.content, + contentWarning = draft.contentWarning, + inReplyToId = draft.inReplyToId, + replyingStatusContent = status.content.parseAsMastodonHtml().toString(), + replyingStatusAuthor = status.account.localUsername, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility, + scheduledAt = draft.scheduledAt, + language = draft.language, + statusId = draft.statusId, + kind = ComposeOptions.ComposeKind.EDIT_DRAFT, + ) + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + startActivity(ComposeActivityIntent(context, intent.pachliAccountId, composeOptions)) + }.onFailure { error -> + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + Timber.w("failed loading reply information: %s", error) + + if (error is ClientError.NotFound) { + // the original status to which a reply was drafted has been deleted + // let's open the ComposeActivity without reply information + Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show() + openDraftWithoutReply(draft) + } else { + Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) + .show() + } + } } } diff --git a/app/src/main/java/app/pachli/components/drafts/DraftsViewModel.kt b/app/src/main/java/app/pachli/components/drafts/DraftsViewModel.kt index d6192a8b45..56f012c8b6 100644 --- a/app/src/main/java/app/pachli/components/drafts/DraftsViewModel.kt +++ b/app/src/main/java/app/pachli/components/drafts/DraftsViewModel.kt @@ -24,9 +24,7 @@ import androidx.paging.cachedIn import app.pachli.core.data.repository.AccountManager import app.pachli.core.database.dao.DraftDao import app.pachli.core.database.model.DraftEntity -import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.NetworkResult import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.launch @@ -63,9 +61,7 @@ class DraftsViewModel @Inject constructor( } } - suspend fun getStatus(statusId: String): NetworkResult { - return api.status(statusId) - } + suspend fun getStatus(statusId: String) = api.status(statusId) override fun onCleared() { viewModelScope.launch { diff --git a/app/src/main/java/app/pachli/components/followedtags/FollowedTagsActivity.kt b/app/src/main/java/app/pachli/components/followedtags/FollowedTagsActivity.kt index 28b8e64014..a81eedcdc5 100644 --- a/app/src/main/java/app/pachli/components/followedtags/FollowedTagsActivity.kt +++ b/app/src/main/java/app/pachli/components/followedtags/FollowedTagsActivity.kt @@ -27,7 +27,8 @@ import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.ui.ActionButtonScrollListener import app.pachli.databinding.ActivityFollowedTagsBinding import app.pachli.interfaces.HashtagActionListener -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -118,58 +119,52 @@ class FollowedTagsActivity : private fun follow(tagName: String, position: Int = -1) { lifecycleScope.launch { - api.followTag(tagName).fold( - { - if (position == -1) { - viewModel.tags.add(it) - } else { - viewModel.tags.add(position, it) - } - viewModel.currentSource?.invalidate() - }, - { - Snackbar.make( - this@FollowedTagsActivity, - binding.followedTagsView, - getString(R.string.error_following_hashtag_format, tagName), - Snackbar.LENGTH_SHORT, - ) - .show() - }, - ) + api.followTag(tagName).onSuccess { + if (position == -1) { + viewModel.tags.add(it.body) + } else { + viewModel.tags.add(position, it.body) + } + viewModel.currentSource?.invalidate() + }.onFailure { + Snackbar.make( + this@FollowedTagsActivity, + binding.followedTagsView, + getString(R.string.error_following_hashtag_format, tagName), + Snackbar.LENGTH_SHORT, + ) + .show() + } } } override fun unfollow(tagName: String, position: Int) { lifecycleScope.launch { - api.unfollowTag(tagName).fold( - { - viewModel.tags.removeAt(position) - Snackbar.make( - this@FollowedTagsActivity, - binding.followedTagsView, - getString(R.string.confirmation_hashtag_unfollowed, tagName), - Snackbar.LENGTH_LONG, - ) - .setAction(R.string.action_undo) { - follow(tagName, position) - } - .show() - viewModel.currentSource?.invalidate() - }, - { - Snackbar.make( - this@FollowedTagsActivity, - binding.followedTagsView, - getString( - R.string.error_unfollowing_hashtag_format, - tagName, - ), - Snackbar.LENGTH_SHORT, - ) - .show() - }, - ) + api.unfollowTag(tagName).onSuccess { + viewModel.tags.removeAt(position) + Snackbar.make( + this@FollowedTagsActivity, + binding.followedTagsView, + getString(R.string.confirmation_hashtag_unfollowed, tagName), + Snackbar.LENGTH_LONG, + ) + .setAction(R.string.action_undo) { + follow(tagName, position) + } + .show() + viewModel.currentSource?.invalidate() + }.onFailure { + Snackbar.make( + this@FollowedTagsActivity, + binding.followedTagsView, + getString( + R.string.error_unfollowing_hashtag_format, + tagName, + ), + Snackbar.LENGTH_SHORT, + ) + .show() + } } } diff --git a/app/src/main/java/app/pachli/components/followedtags/FollowedTagsRemoteMediator.kt b/app/src/main/java/app/pachli/components/followedtags/FollowedTagsRemoteMediator.kt index 3e5068e653..fc16232791 100644 --- a/app/src/main/java/app/pachli/components/followedtags/FollowedTagsRemoteMediator.kt +++ b/app/src/main/java/app/pachli/components/followedtags/FollowedTagsRemoteMediator.kt @@ -7,10 +7,10 @@ import androidx.paging.RemoteMediator import app.pachli.core.network.model.HashTag import app.pachli.core.network.model.HttpHeaderLink import app.pachli.core.network.retrofit.MastodonApi +import app.pachli.core.network.retrofit.apiresult.ApiResult +import com.github.michaelbull.result.getOrElse import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive -import retrofit2.HttpException -import retrofit2.Response @OptIn(ExperimentalPagingApi::class) class FollowedTagsRemoteMediator( @@ -32,7 +32,7 @@ class FollowedTagsRemoteMediator( } } - private suspend fun request(loadType: LoadType): Response>? { + private suspend fun request(loadType: LoadType): ApiResult>? { return when (loadType) { LoadType.PREPEND -> null LoadType.APPEND -> api.followedTags(maxId = viewModel.nextKey) @@ -44,13 +44,13 @@ class FollowedTagsRemoteMediator( } } - private fun applyResponse(response: Response>): MediatorResult { - val tags = response.body() - if (!response.isSuccessful || tags == null) { - return MediatorResult.Error(HttpException(response)) + private fun applyResponse(result: ApiResult>): MediatorResult { + val response = result.getOrElse { + return MediatorResult.Error(it.throwable) } + val tags = response.body - val links = HttpHeaderLink.parse(response.headers()["Link"]) + val links = HttpHeaderLink.parse(response.headers["Link"]) viewModel.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") viewModel.tags.addAll(tags) viewModel.currentSource?.invalidate() diff --git a/app/src/main/java/app/pachli/components/followedtags/FollowedTagsViewModel.kt b/app/src/main/java/app/pachli/components/followedtags/FollowedTagsViewModel.kt index f52b957dc5..3eb6e00bd0 100644 --- a/app/src/main/java/app/pachli/components/followedtags/FollowedTagsViewModel.kt +++ b/app/src/main/java/app/pachli/components/followedtags/FollowedTagsViewModel.kt @@ -12,7 +12,7 @@ import app.pachli.core.network.model.HashTag import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.SharedPreferencesRepository -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.mapBoth import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted @@ -52,7 +52,8 @@ class FollowedTagsViewModel @Inject constructor( suspend fun searchAutocompleteSuggestions(token: String): List { return api.search(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) - .fold({ searchResult -> + .mapBoth({ + val searchResult = it.body searchResult.hashtags.map { ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult( hashtag = it.name, @@ -60,7 +61,7 @@ class FollowedTagsViewModel @Inject constructor( ) } }, { e -> - Timber.e(e, "Autocomplete search for %s failed.", token) + Timber.e("Autocomplete search for %s failed: %s", token, e) emptyList() }) } 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 c61ca75b0a..28d95caf77 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 @@ -23,8 +23,6 @@ import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import timber.log.Timber @@ -103,18 +101,9 @@ class InstanceListFragment : } viewLifecycleOwner.lifecycleScope.launch { - try { - 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())) - } - } catch (e: Exception) { - currentCoroutineContext().ensureActive() - onFetchInstancesFailure(e) - } + api.domainBlocks(id, bottomId) + .onSuccess { onFetchInstancesSuccess(it.body, it.headers["Link"]) } + .onFailure { onFetchInstancesFailure(it.throwable) } } } diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationFetcher.kt b/app/src/main/java/app/pachli/components/notifications/NotificationFetcher.kt index da5efd9a10..0fb8620439 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationFetcher.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationFetcher.kt @@ -35,6 +35,7 @@ import app.pachli.core.network.retrofit.MastodonApi import app.pachli.worker.NotificationWorker import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import dagger.hilt.android.qualifiers.ApplicationContext @@ -242,20 +243,21 @@ class NotificationFetcher @Inject constructor( } private suspend fun fetchMarker(account: AccountEntity): Marker? { - return try { - val allMarkers = mastodonApi.markersWithAuth( - account.authHeader, - account.domain, - listOf("notifications"), - ) - val notificationMarker = allMarkers["notifications"] - Timber.d("Fetched marker for %s: %s", account.fullName, notificationMarker) - notificationMarker - } catch (e: Exception) { - currentCoroutineContext().ensureActive() - Timber.e(e, "Failed to fetch marker") - null - } + return mastodonApi.markersWithAuth( + account.authHeader, + account.domain, + listOf("notifications"), + ).mapBoth( + { + val notificationMarker = it.body["notifications"] + Timber.d("Fetched marker for %s: %s", account.fullName, notificationMarker) + notificationMarker + }, + { + Timber.e("Failed to fetch marker: %s", it) + null + }, + ) } companion object { diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt index b929feb4e1..437c888383 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt @@ -63,7 +63,6 @@ import app.pachli.core.network.model.Status import app.pachli.core.preferences.TabTapBehaviour import app.pachli.core.ui.ActionButtonScrollListener import app.pachli.core.ui.BackgroundMessage -import app.pachli.core.ui.extensions.getErrorString import app.pachli.core.ui.makeIcon import app.pachli.databinding.FragmentTimelineNotificationsBinding import app.pachli.fragment.SFragment @@ -96,7 +95,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import postPrepend -import timber.log.Timber @AndroidEntryPoint class NotificationsFragment : @@ -280,9 +278,8 @@ class NotificationsFragment : uiResult.onFailure { uiError -> val message = getString( uiError.message, - uiError.throwable.getErrorString(requireContext()), + uiError.error.fmt(requireContext()), ) - Timber.d(uiError.throwable, message) val snackbar = Snackbar.make( // Without this the FAB will not move out of the way (activity as ActionButtonActivity).actionButton ?: binding.root, 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 dcf6a67638..be4fd764c9 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationsViewModel.kt @@ -25,6 +25,7 @@ import androidx.paging.cachedIn import androidx.paging.filter import androidx.paging.map import app.pachli.R +import app.pachli.core.common.PachliError import app.pachli.core.common.extensions.throttleFirst import app.pachli.core.data.model.StatusViewData import app.pachli.core.data.repository.AccountManager @@ -56,14 +57,13 @@ import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result import com.github.michaelbull.result.mapEither +import com.github.michaelbull.result.onFailure import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -83,7 +83,6 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import retrofit2.HttpException data class UiState( /** Filtered notification types */ @@ -288,8 +287,8 @@ sealed interface StatusActionSuccess : UiSuccess { /** Errors from fallible view model actions that the UI will need to show */ sealed interface UiError { - /** The exception associated with the error */ - val throwable: Throwable + /** The error associated with the error */ + val error: PachliError /** The action that failed. Can be resent to retry the action */ val action: UiAction? @@ -299,62 +298,62 @@ sealed interface UiError { val message: Int data class ClearNotifications( - override val throwable: Throwable, + override val error: PachliError, override val action: FallibleUiAction.ClearNotifications = FallibleUiAction.ClearNotifications, override val message: Int = R.string.ui_error_clear_notifications, ) : UiError data class Bookmark( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.Bookmark, override val message: Int = R.string.ui_error_bookmark_fmt, ) : UiError data class Favourite( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.Favourite, override val message: Int = R.string.ui_error_favourite_fmt, ) : UiError data class Reblog( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.Reblog, override val message: Int = R.string.ui_error_reblog_fmt, ) : UiError data class VoteInPoll( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.VoteInPoll, override val message: Int = R.string.ui_error_vote_fmt, ) : UiError data class AcceptFollowRequest( - override val throwable: Throwable, + override val error: PachliError, override val action: NotificationAction.AcceptFollowRequest, override val message: Int = R.string.ui_error_accept_follow_request, ) : UiError data class RejectFollowRequest( - override val throwable: Throwable, + override val error: PachliError, override val action: NotificationAction.RejectFollowRequest, override val message: Int = R.string.ui_error_reject_follow_request, ) : UiError data class GetFilters( - override val throwable: Throwable, + override val error: PachliError, override val action: UiAction? = null, override val message: Int = R.string.ui_error_filter_v1_load_fmt, ) : UiError companion object { - fun make(throwable: Throwable, action: FallibleUiAction) = when (action) { - is StatusAction.Bookmark -> Bookmark(throwable, action) - is StatusAction.Favourite -> Favourite(throwable, action) - is StatusAction.Reblog -> Reblog(throwable, action) - is StatusAction.VoteInPoll -> VoteInPoll(throwable, action) - is NotificationAction.AcceptFollowRequest -> AcceptFollowRequest(throwable, action) - is NotificationAction.RejectFollowRequest -> RejectFollowRequest(throwable, action) - FallibleUiAction.ClearNotifications -> ClearNotifications(throwable) + fun make(error: PachliError, action: FallibleUiAction) = when (action) { + is StatusAction.Bookmark -> Bookmark(error, action) + is StatusAction.Favourite -> Favourite(error, action) + is StatusAction.Reblog -> Reblog(error, action) + is StatusAction.VoteInPoll -> VoteInPoll(error, action) + is NotificationAction.AcceptFollowRequest -> AcceptFollowRequest(error, action) + is NotificationAction.RejectFollowRequest -> RejectFollowRequest(error, action) + FallibleUiAction.ClearNotifications -> ClearNotifications(error) } } } @@ -460,18 +459,15 @@ class NotificationsViewModel @AssistedInject constructor( uiAction.filterIsInstance() .throttleFirst() .collect { action -> - val result = try { - when (action) { - is NotificationAction.AcceptFollowRequest -> - timelineCases.acceptFollowRequest(action.accountId) - is NotificationAction.RejectFollowRequest -> - timelineCases.rejectFollowRequest(action.accountId) - } - Ok(NotificationActionSuccess.from(action)) - } catch (e: Exception) { - currentCoroutineContext().ensureActive() - Err(UiError.make(e, action)) - } + val result = when (action) { + is NotificationAction.AcceptFollowRequest -> + timelineCases.acceptFollowRequest(action.accountId) + is NotificationAction.RejectFollowRequest -> + timelineCases.rejectFollowRequest(action.accountId) + }.mapEither( + { NotificationActionSuccess.from(action) }, + { UiError.make(it, action) }, + ) _uiResult.send(result) } } @@ -508,7 +504,7 @@ class NotificationsViewModel @AssistedInject constructor( ) }.mapEither( { StatusActionSuccess.from(action) }, - { UiError.make(it.throwable, action) }, + { UiError.make(it.error, action) }, ) _uiResult.send(result) } @@ -586,14 +582,8 @@ class NotificationsViewModel @AssistedInject constructor( } private suspend fun onClearNotifications(action: FallibleUiAction.ClearNotifications) { - try { - repository.clearNotifications().apply { - if (!isSuccessful) _uiResult.send(Err(UiError.make(HttpException(this), action))) - } - } catch (e: Exception) { - currentCoroutineContext().ensureActive() - _uiResult.send(Err(UiError.make(e, action))) - } + repository.clearNotifications() + .onFailure { _uiResult.send(Err(UiError.make(it, action))) } } private suspend fun getNotifications( diff --git a/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt index 1e5bebb21b..bdef954cb0 100644 --- a/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt @@ -48,7 +48,6 @@ import app.pachli.core.navigation.LoginActivityIntent.LoginMode import app.pachli.core.navigation.PreferencesActivityIntent import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen import app.pachli.core.navigation.TabPreferenceActivityIntent -import app.pachli.core.network.model.Account import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.preferences.PrefKeys @@ -62,6 +61,8 @@ import app.pachli.util.getInitialLanguages import app.pachli.util.getLocaleList import app.pachli.util.getPachliDisplayName import app.pachli.util.iconRes +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import com.google.android.material.snackbar.Snackbar import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial @@ -70,9 +71,6 @@ import javax.inject.Inject import kotlin.properties.Delegates import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.launch -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import timber.log.Timber @AndroidEntryPoint @@ -331,34 +329,25 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null, language: String? = null) { // TODO these could also be "datastore backed" preferences (a ServerPreferenceDataStore); // follow-up of issue #3204 - - mastodonApi.accountUpdateSource(visibility, sensitive, language) - .enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - val account = response.body() - if (response.isSuccessful && account != null) { - accountManager.activeAccount?.let { - accountManager.setDefaultPostPrivacy( - it.id, - account.source?.privacy - ?: Status.Visibility.PUBLIC, - ) - accountManager.setDefaultMediaSensitivity(it.id, account.source?.sensitive ?: false) - accountManager.setDefaultPostLanguage(it.id, language.orEmpty()) - } - } else { - Timber.e("failed updating settings on server") - showErrorSnackbar(visibility, sensitive) - } - } - - override fun onFailure(call: Call, t: Throwable) { - Timber.e(t, "failed updating settings on server") - showErrorSnackbar(visibility, sensitive) + lifecycleScope.launch { + mastodonApi.accountUpdateSource(visibility, sensitive, language) + .onSuccess { + val account = it.body + accountManager.activeAccount?.let { + accountManager.setDefaultPostPrivacy( + it.id, + account.source?.privacy + ?: Status.Visibility.PUBLIC, + ) + accountManager.setDefaultMediaSensitivity(it.id, account.source?.sensitive ?: false) + accountManager.setDefaultPostLanguage(it.id, language.orEmpty()) } - }, - ) + } + .onFailure { + Timber.e("failed updating settings on server: %s", it) + showErrorSnackbar(visibility, sensitive) + } + } } private fun showErrorSnackbar(visibility: String?, sensitive: Boolean?) { diff --git a/app/src/main/java/app/pachli/components/report/ReportViewModel.kt b/app/src/main/java/app/pachli/components/report/ReportViewModel.kt index 143106a947..6e977a5685 100644 --- a/app/src/main/java/app/pachli/components/report/ReportViewModel.kt +++ b/app/src/main/java/app/pachli/components/report/ReportViewModel.kt @@ -41,7 +41,6 @@ import app.pachli.util.Error import app.pachli.util.Loading import app.pachli.util.Resource import app.pachli.util.Success -import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel @@ -173,17 +172,15 @@ class ReportViewModel @Inject constructor( } else { mastodonApi.muteAccount(accountId) } - .onSuccess { response -> - val relationship = response.body + .onSuccess { + val relationship = it.body val muting = relationship.muting muteStateMutable.value = Success(muting) if (muting) { eventHub.dispatch(MuteEvent(accountId)) } } - .onFailure { t -> - muteStateMutable.value = Error(false, t.throwable.message) - } + .onFailure { muteStateMutable.value = Error(false, it.throwable.message) } } muteStateMutable.value = Loading() @@ -197,16 +194,16 @@ class ReportViewModel @Inject constructor( } else { mastodonApi.blockAccount(accountId) } - .onSuccess { response -> - val relationship = response.body + .onSuccess { + val relationship = it.body val blocking = relationship.blocking blockStateMutable.value = Success(blocking) if (blocking) { eventHub.dispatch(BlockEvent(accountId)) } } - .onFailure { t -> - blockStateMutable.value = Error(false, t.throwable.message) + .onFailure { + blockStateMutable.value = Error(false, it.throwable.message) } } blockStateMutable.value = Loading() @@ -216,11 +213,8 @@ class ReportViewModel @Inject constructor( reportingStateMutable.value = Loading() viewModelScope.launch { mastodonApi.report(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null) - .fold({ - reportingStateMutable.value = Success(true) - }, { error -> - reportingStateMutable.value = Error(cause = error) - }) + .onSuccess { reportingStateMutable.value = Success(true) } + .onFailure { error -> reportingStateMutable.value = Error(cause = error.throwable) } } } 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 d23aad23a9..c22de1382a 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 @@ -20,6 +20,7 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi +import com.github.michaelbull.result.get import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.currentCoroutineContext @@ -78,7 +79,7 @@ class StatusesPagingSource( } private suspend fun getSingleStatus(statusId: String): Status? { - return mastodonApi.status(statusId).getOrNull() + return mastodonApi.status(statusId).get()?.body } private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List? { @@ -89,6 +90,6 @@ class StatusesPagingSource( minId = minId, limit = limit, excludeReblogs = true, - ).body() + ).get()?.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 4502a54060..20ea42fb4c 100644 --- a/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusPagingSource.kt +++ b/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusPagingSource.kt @@ -20,7 +20,7 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import app.pachli.core.network.model.ScheduledStatus import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.getOrElse +import com.github.michaelbull.result.mapBoth class ScheduledStatusPagingSourceFactory( private val mastodonApi: MastodonApi, @@ -59,12 +59,16 @@ class ScheduledStatusPagingSource( nextKey = scheduledStatusesCache.lastOrNull()?.id, ) } else { - val result = mastodonApi.scheduledStatuses( + mastodonApi.scheduledStatuses( maxId = params.key, limit = params.loadSize, - ).getOrElse { return LoadResult.Error(it) } - - LoadResult.Page(data = result, prevKey = null, nextKey = result.lastOrNull()?.id) + ).mapBoth( + { + val result = it.body + LoadResult.Page(data = result, prevKey = null, nextKey = result.lastOrNull()?.id) + }, + { LoadResult.Error(it.throwable) }, + ) } } } diff --git a/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusViewModel.kt b/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusViewModel.kt index cfadf814fc..f06bcfbcaa 100644 --- a/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusViewModel.kt +++ b/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusViewModel.kt @@ -24,7 +24,8 @@ import androidx.paging.cachedIn import app.pachli.core.eventhub.EventHub import app.pachli.core.network.model.ScheduledStatus import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.launch @@ -46,14 +47,9 @@ class ScheduledStatusViewModel @Inject constructor( fun deleteScheduledStatus(status: ScheduledStatus) { viewModelScope.launch { - mastodonApi.deleteScheduledStatus(status.id).fold( - { - pagingSourceFactory.remove(status) - }, - { throwable -> - Timber.w(throwable, "Error deleting scheduled status") - }, - ) + mastodonApi.deleteScheduledStatus(status.id) + .onSuccess { pagingSourceFactory.remove(status) } + .onFailure { Timber.w("Error deleting scheduled status: %s", it) } } } } diff --git a/app/src/main/java/app/pachli/components/search/SearchViewModel.kt b/app/src/main/java/app/pachli/components/search/SearchViewModel.kt index 169860f0ee..884cc1226b 100644 --- a/app/src/main/java/app/pachli/components/search/SearchViewModel.kt +++ b/app/src/main/java/app/pachli/components/search/SearchViewModel.kt @@ -56,13 +56,13 @@ import app.pachli.core.network.model.DeletedStatus import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi +import app.pachli.core.network.retrofit.apiresult.ApiResult import app.pachli.usecase.TimelineCases import app.pachli.util.getInitialLanguages import app.pachli.util.getLocaleList -import at.connyduck.calladapter.networkresult.NetworkResult -import at.connyduck.calladapter.networkresult.fold -import at.connyduck.calladapter.networkresult.onFailure import com.github.michaelbull.result.mapBoth +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import io.github.z4kn4fein.semver.constraints.toConstraint import javax.inject.Inject @@ -259,11 +259,12 @@ class SearchViewModel @Inject constructor( fun removeItem(statusViewData: StatusViewData) { viewModelScope.launch { - if (timelineCases.delete(statusViewData.id).isSuccess) { - if (loadedStatuses.remove(statusViewData)) { - statusesPagingSourceFactory.invalidate() + timelineCases.delete(statusViewData.id) + .onSuccess { + if (loadedStatuses.remove(statusViewData)) { + statusesPagingSourceFactory.invalidate() + } } - } } } @@ -273,16 +274,16 @@ class SearchViewModel @Inject constructor( fun reblog(statusViewData: StatusViewData, reblog: Boolean) { viewModelScope.launch { - timelineCases.reblog(statusViewData.id, reblog).fold({ - updateStatus( - statusViewData.status.copy( - reblogged = reblog, - reblog = statusViewData.status.reblog?.copy(reblogged = reblog), - ), - ) - }, { t -> - Timber.d(t, "Failed to reblog status %s", statusViewData.id) - }) + timelineCases.reblog(statusViewData.id, reblog) + .onSuccess { + updateStatus( + statusViewData.status.copy( + reblogged = reblog, + reblog = statusViewData.status.reblog?.copy(reblogged = reblog), + ), + ) + } + .onFailure { Timber.d("Failed to reblog status %s: %s", statusViewData.id, it) } } } @@ -299,7 +300,7 @@ class SearchViewModel @Inject constructor( updateStatus(statusViewData.status.copy(poll = votedPoll)) viewModelScope.launch { timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices) - .onFailure { t -> Timber.d(t, "Failed to vote in poll: %s", statusViewData.id) } + .onFailure { Timber.d("Failed to vote in poll: %s: %s", statusViewData.id, it) } } } @@ -335,7 +336,7 @@ class SearchViewModel @Inject constructor( } } - fun deleteStatusAsync(id: String): Deferred> { + fun deleteStatusAsync(id: String): Deferred> { return viewModelScope.async { timelineCases.delete(id) } @@ -354,10 +355,10 @@ class SearchViewModel @Inject constructor( // knows about, therefore the accounts that posted those statuses will definitely // be known by the server and there is no need to resolve them further. return mastodonApi.search(query = token, resolve = false, type = SearchType.Account.apiParameter, limit = 10) - .fold( - { it.accounts.map { ComposeAutoCompleteAdapter.AutocompleteResult.AccountResult(it) } }, + .mapBoth( + { it.body.accounts.map { ComposeAutoCompleteAdapter.AutocompleteResult.AccountResult(it) } }, { - Timber.e(it, "Autocomplete search for %s failed.", token) + Timber.e("Autocomplete search for %s failed: %s", token, it) emptyList() }, ) 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 90406b9c10..de101593db 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 @@ -21,7 +21,7 @@ import androidx.paging.PagingState import app.pachli.components.search.SearchType import app.pachli.core.network.model.SearchResult import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.getOrElse +import com.github.michaelbull.result.getOrElse import timber.log.Timber class SearchPagingSource( @@ -63,9 +63,9 @@ class SearchPagingSource( offset = currentKey, following = false, ).getOrElse { - Timber.w(it) - return LoadResult.Error(it) - } + Timber.w(it.throwable) + return LoadResult.Error(it.throwable) + }.body val res = parser(data) diff --git a/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt index 55ed30d072..979239c40b 100644 --- a/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt @@ -55,7 +55,8 @@ import app.pachli.core.network.model.Status.Mention import app.pachli.core.ui.ClipboardUseCase import app.pachli.interfaces.StatusActionListener import app.pachli.view.showMuteAccountDialog -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -414,38 +415,36 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis .setMessage(R.string.dialog_redraft_post_warning) .setPositiveButton(android.R.string.ok) { _, _ -> lifecycleScope.launch { - viewModel.deleteStatusAsync(statusViewData.id).await().fold( - { deletedStatus -> - viewModel.removeItem(statusViewData) - - val redraftStatus = if (deletedStatus.isEmpty()) { - statusViewData.status.toDeletedStatus() - } else { - deletedStatus - } - - val intent = ComposeActivityIntent( - requireContext(), - pachliAccountId, - ComposeOptions( - content = redraftStatus.text.orEmpty(), - inReplyToId = redraftStatus.inReplyToId, - visibility = redraftStatus.visibility, - contentWarning = redraftStatus.spoilerText, - mediaAttachments = redraftStatus.attachments, - sensitive = redraftStatus.sensitive, - poll = redraftStatus.poll?.toNewPoll(redraftStatus.createdAt), - language = redraftStatus.language, - kind = ComposeOptions.ComposeKind.NEW, - ), - ) - startActivity(intent) - }, - { error -> - Timber.w(error, "error deleting status") - Toast.makeText(context, app.pachli.core.ui.R.string.error_generic, Toast.LENGTH_SHORT).show() - }, - ) + viewModel.deleteStatusAsync(statusViewData.id).await().onSuccess { + val deletedStatus = it.body + viewModel.removeItem(statusViewData) + + val redraftStatus = if (deletedStatus.isEmpty()) { + statusViewData.status.toDeletedStatus() + } else { + deletedStatus + } + + val intent = ComposeActivityIntent( + requireContext(), + pachliAccountId, + ComposeOptions( + content = redraftStatus.text.orEmpty(), + inReplyToId = redraftStatus.inReplyToId, + visibility = redraftStatus.visibility, + contentWarning = redraftStatus.spoilerText, + mediaAttachments = redraftStatus.attachments, + sensitive = redraftStatus.sensitive, + poll = redraftStatus.poll?.toNewPoll(redraftStatus.createdAt), + language = redraftStatus.language, + kind = ComposeOptions.ComposeKind.NEW, + ), + ) + startActivity(intent) + }.onFailure { error -> + Timber.w("error deleting status: %s", error) + Toast.makeText(context, app.pachli.core.ui.R.string.error_generic, Toast.LENGTH_SHORT).show() + } } } .setNegativeButton(android.R.string.cancel, null) @@ -455,30 +454,28 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis private fun editStatus(pachliAccountId: Long, id: String, status: Status) { lifecycleScope.launch { - mastodonApi.statusSource(id).fold( - { source -> - val composeOptions = ComposeOptions( - content = source.text, - inReplyToId = status.inReplyToId, - visibility = status.visibility, - contentWarning = source.spoilerText, - mediaAttachments = status.attachments, - sensitive = status.sensitive, - language = status.language, - statusId = source.id, - poll = status.poll?.toNewPoll(status.createdAt), - kind = ComposeOptions.ComposeKind.EDIT_POSTED, - ) - startActivity(ComposeActivityIntent(requireContext(), pachliAccountId, composeOptions)) - }, - { - Snackbar.make( - requireView(), - getString(R.string.error_status_source_load), - Snackbar.LENGTH_SHORT, - ).show() - }, - ) + mastodonApi.statusSource(id).onSuccess { response -> + val source = response.body + val composeOptions = ComposeOptions( + content = source.text, + inReplyToId = status.inReplyToId, + visibility = status.visibility, + contentWarning = source.spoilerText, + mediaAttachments = status.attachments, + sensitive = status.sensitive, + language = status.language, + statusId = source.id, + poll = status.poll?.toNewPoll(status.createdAt), + kind = ComposeOptions.ComposeKind.EDIT_POSTED, + ) + startActivity(ComposeActivityIntent(requireContext(), pachliAccountId, composeOptions)) + }.onFailure { + Snackbar.make( + requireView(), + getString(R.string.error_status_source_load), + Snackbar.LENGTH_SHORT, + ).show() + } } } diff --git a/app/src/main/java/app/pachli/components/timeline/CachedTimelineRepository.kt b/app/src/main/java/app/pachli/components/timeline/CachedTimelineRepository.kt index 6aef69d053..243481e7f0 100644 --- a/app/src/main/java/app/pachli/components/timeline/CachedTimelineRepository.kt +++ b/app/src/main/java/app/pachli/components/timeline/CachedTimelineRepository.kt @@ -42,8 +42,9 @@ import app.pachli.core.database.model.TranslationState import app.pachli.core.model.Timeline import app.pachli.core.network.model.Translation import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.NetworkResult -import at.connyduck.calladapter.networkresult.fold +import app.pachli.core.network.retrofit.apiresult.ApiResult +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope @@ -158,31 +159,30 @@ class CachedTimelineRepository @Inject constructor( statusDao.clearWarning(pachliAccountId, statusId) }.join() - suspend fun translate(statusViewData: StatusViewData): NetworkResult { + suspend fun translate(statusViewData: StatusViewData): ApiResult { saveStatusViewData(statusViewData.copy(translationState = TranslationState.TRANSLATING)) val translation = mastodonApi.translate(statusViewData.actionableId) - translation.fold( - { - translatedStatusDao.upsert( - TranslatedStatusEntity( - serverId = statusViewData.actionableId, - timelineUserId = statusViewData.pachliAccountId, - // TODO: Should this embed the network type instead of copying data - // from one type to another? - content = it.content, - spoilerText = it.spoilerText, - poll = it.poll, - attachments = it.attachments, - provider = it.provider, - ), - ) - saveStatusViewData(statusViewData.copy(translationState = TranslationState.SHOW_TRANSLATION)) - }, - { - // Reset the translation state - saveStatusViewData(statusViewData) - }, - ) + translation.onSuccess { + val body = it.body + translatedStatusDao.upsert( + TranslatedStatusEntity( + serverId = statusViewData.actionableId, + timelineUserId = statusViewData.pachliAccountId, + // TODO: Should this embed the network type instead of copying data + // from one type to another? + content = body.content, + spoilerText = body.spoilerText, + poll = body.poll, + attachments = body.attachments, + provider = body.provider, + ), + ) + saveStatusViewData(statusViewData.copy(translationState = TranslationState.SHOW_TRANSLATION)) + }.onFailure { + // Reset the translation state + saveStatusViewData(statusViewData) + } + return translation } diff --git a/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt b/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt index f411367b0e..e548d42f6a 100644 --- a/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt @@ -72,7 +72,6 @@ import app.pachli.core.network.model.Status import app.pachli.core.preferences.TabTapBehaviour import app.pachli.core.ui.ActionButtonScrollListener import app.pachli.core.ui.BackgroundMessage -import app.pachli.core.ui.extensions.getErrorString import app.pachli.databinding.FragmentTimelineBinding import app.pachli.fragment.SFragment import app.pachli.interfaces.ActionButtonActivity @@ -289,9 +288,8 @@ class TimelineFragment : uiResult.onFailure { uiError -> val message = getString( uiError.message, - uiError.throwable.getErrorString(requireContext()), + uiError.error.fmt(requireContext()), ) - Timber.d(uiError.throwable, message) snackbar?.dismiss() snackbar = Snackbar.make( // Without this the FAB will not move out of the way diff --git a/app/src/main/java/app/pachli/components/timeline/util/TimelineUtils.kt b/app/src/main/java/app/pachli/components/timeline/util/TimelineUtils.kt deleted file mode 100644 index a51bf971fb..0000000000 --- a/app/src/main/java/app/pachli/components/timeline/util/TimelineUtils.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.pachli.components.timeline.util - -import java.io.IOException -import retrofit2.HttpException - -fun Throwable.isExpected() = this is IOException || this is HttpException - -inline fun ifExpected( - t: Throwable, - cb: () -> T, -): T { - if (t.isExpected()) { - return cb() - } else { - throw t - } -} diff --git a/app/src/main/java/app/pachli/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt b/app/src/main/java/app/pachli/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt index 32be199c8d..00ba7324ba 100644 --- a/app/src/main/java/app/pachli/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt +++ b/app/src/main/java/app/pachli/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt @@ -34,13 +34,16 @@ import app.pachli.core.database.model.TimelineStatusWithAccount import app.pachli.core.network.model.Links import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi +import app.pachli.core.network.retrofit.apiresult.ApiResponse +import app.pachli.core.network.retrofit.apiresult.ApiResult +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.get +import com.github.michaelbull.result.getOrElse import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import okhttp3.Headers -import retrofit2.HttpException -import retrofit2.Response import timber.log.Timber @OptIn(ExperimentalPagingApi::class) @@ -92,12 +95,9 @@ class CachedTimelineRemoteMediator( Timber.d("Append from remoteKey: %s", rke) mastodonApi.homeTimeline(maxId = rke.key, limit = state.config.pageSize) } - } + }.getOrElse { return@transactionProvider MediatorResult.Error(it.throwable) } - val statuses = response.body() - if (!response.isSuccessful || statuses == null) { - return@transactionProvider MediatorResult.Error(HttpException(response)) - } + val statuses = response.body Timber.d("%d - # statuses loaded", statuses.size) @@ -109,7 +109,7 @@ class CachedTimelineRemoteMediator( Timber.d(" %s..%s", statuses.first().id, statuses.last().id) - val links = Links.from(response.headers()["link"]) + val links = Links.from(response.headers["link"]) when (loadType) { LoadType.REFRESH -> { @@ -176,7 +176,7 @@ class CachedTimelineRemoteMediator( * @return The initial page of statuses centered on the status with [statusId], * or the most recent statuses if [statusId] is null. */ - private suspend fun getInitialPage(statusId: String?, pageSize: Int): Response> = coroutineScope { + private suspend fun getInitialPage(statusId: String?, pageSize: Int): ApiResult> = coroutineScope { statusId ?: return@coroutineScope mastodonApi.homeTimeline(limit = pageSize) val status = async { mastodonApi.status(statusId = statusId) } @@ -184,9 +184,9 @@ class CachedTimelineRemoteMediator( val nextPage = async { mastodonApi.homeTimeline(maxId = statusId, limit = pageSize * 3) } val statuses = buildList { - prevPage.await().body()?.let { this.addAll(it) } - status.await().getOrNull()?.let { this.add(it) } - nextPage.await().body()?.let { this.addAll(it) } + prevPage.await().get()?.let { this.addAll(it.body) } + status.await().get()?.let { this.add(it.body) } + nextPage.await().get()?.let { this.addAll(it.body) } } val minId = statuses.firstOrNull()?.id ?: statusId @@ -196,7 +196,7 @@ class CachedTimelineRemoteMediator( .add("link: ; rel=\"next\", ; rel=\"prev\"") .build() - return@coroutineScope Response.success(statuses, headers) + return@coroutineScope Ok(ApiResponse(headers, statuses, 200)) } /** diff --git a/app/src/main/java/app/pachli/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt b/app/src/main/java/app/pachli/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt index 44d57001a2..e41bddc578 100644 --- a/app/src/main/java/app/pachli/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt +++ b/app/src/main/java/app/pachli/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt @@ -27,11 +27,10 @@ import app.pachli.core.database.model.AccountEntity import app.pachli.core.model.Timeline import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi -import java.io.IOException +import app.pachli.core.network.retrofit.apiresult.ApiResult +import com.github.michaelbull.result.getOrElse import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive -import retrofit2.HttpException -import retrofit2.Response import timber.log.Timber /** Remote mediator for accessing timelines that are not backed by the database. */ @@ -78,8 +77,8 @@ class NetworkTimelineRemoteMediator( Timber.d("- load(), type = %s, key = %s", loadType, key) - val response = fetchStatusPageByKind(loadType, key, state.config.initialLoadSize) - val page = Page.tryFrom(response).getOrElse { return MediatorResult.Error(it) } + val page = Page.tryFrom(fetchStatusPageByKind(loadType, key, state.config.initialLoadSize)) + .getOrElse { return MediatorResult.Error(it.throwable) } val endOfPaginationReached = page.data.isEmpty() if (!endOfPaginationReached) { @@ -105,8 +104,7 @@ class NetworkTimelineRemoteMediator( } } - @Throws(IOException::class, HttpException::class, IllegalStateException::class) - private suspend fun fetchStatusPageByKind(loadType: LoadType, key: String?, loadSize: Int): Response> { + private suspend fun fetchStatusPageByKind(loadType: LoadType, key: String?, loadSize: Int): ApiResult> { val (maxId, minId) = when (loadType) { // When refreshing fetch a page of statuses that are immediately *newer* than the key // This is so that the user's reading position is not lost. diff --git a/app/src/main/java/app/pachli/components/timeline/viewmodel/PageCache.kt b/app/src/main/java/app/pachli/components/timeline/viewmodel/PageCache.kt index d5dbb1fb13..f7bdfe6fbb 100644 --- a/app/src/main/java/app/pachli/components/timeline/viewmodel/PageCache.kt +++ b/app/src/main/java/app/pachli/components/timeline/viewmodel/PageCache.kt @@ -22,11 +22,11 @@ import androidx.paging.LoadType import app.pachli.BuildConfig import app.pachli.core.network.model.Links import app.pachli.core.network.model.Status +import app.pachli.core.network.retrofit.apiresult.ApiError +import app.pachli.core.network.retrofit.apiresult.ApiResult +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.map import java.util.LinkedList -import kotlin.Result.Companion.failure -import kotlin.Result.Companion.success -import retrofit2.HttpException -import retrofit2.Response import timber.log.Timber /** A page of data from the Mastodon API */ @@ -47,22 +47,15 @@ data class Page( override fun toString() = "size: ${"%2d".format(data.size)}, range: ${data.firstOrNull()?.id}..${data.lastOrNull()?.id}, prevKey: $prevKey, nextKey: $nextKey" companion object { - fun tryFrom(response: Response>): Result { - val statuses = response.body() - if (!response.isSuccessful || statuses == null) { - return failure(HttpException(response)) - } - - val links = Links.from(response.headers()["link"]) - Timber.d(" link: %s", response.headers()["link"]) - Timber.d(" %d - # statuses loaded", statuses.size) - - return success( - Page( - data = statuses.toMutableList(), - nextKey = links.next, - prevKey = links.prev, - ), + fun tryFrom(response: ApiResult>): Result = response.map { + val links = Links.from(it.headers["link"]) + Timber.d(" link: %s", links) + Timber.d(" %d - # statuses loaded", it.body.size) + + Page( + data = it.body.toMutableList(), + nextKey = links.next, + prevKey = links.prev, ) } } diff --git a/app/src/main/java/app/pachli/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/app/pachli/components/timeline/viewmodel/TimelineViewModel.kt index 347df8de32..c6d352b014 100644 --- a/app/src/main/java/app/pachli/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/app/pachli/components/timeline/viewmodel/TimelineViewModel.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import app.pachli.R import app.pachli.components.timeline.TimelineRepository +import app.pachli.core.common.PachliError import app.pachli.core.common.extensions.throttleFirst import app.pachli.core.data.model.StatusViewData import app.pachli.core.data.repository.AccountManager @@ -57,13 +58,10 @@ import app.pachli.core.preferences.SharedPreferencesRepository import app.pachli.core.preferences.TabTapBehaviour import app.pachli.network.ContentFilterModel import app.pachli.usecase.TimelineCases -import at.connyduck.calladapter.networkresult.getOrThrow -import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result +import com.github.michaelbull.result.mapEither import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -223,7 +221,7 @@ sealed interface StatusActionSuccess : UiSuccess { /** Errors from fallible view model actions that the UI will need to show */ sealed interface UiError { /** The throwable associated with the error */ - val throwable: Throwable + val error: PachliError /** The action that failed. Can be resent to retry the action */ val action: UiAction? @@ -233,48 +231,48 @@ sealed interface UiError { val message: Int data class Bookmark( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.Bookmark, override val message: Int = R.string.ui_error_bookmark_fmt, ) : UiError data class Favourite( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.Favourite, override val message: Int = R.string.ui_error_favourite_fmt, ) : UiError data class Reblog( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.Reblog, override val message: Int = R.string.ui_error_reblog_fmt, ) : UiError data class VoteInPoll( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.VoteInPoll, override val message: Int = R.string.ui_error_vote_fmt, ) : UiError data class TranslateStatus( - override val throwable: Throwable, + override val error: PachliError, override val action: StatusAction.Translate, override val message: Int = R.string.ui_error_translate_status_fmt, ) : UiError data class GetFilters( - override val throwable: Throwable, + override val error: PachliError, override val action: UiAction? = null, override val message: Int = R.string.ui_error_filter_v1_load_fmt, ) : UiError companion object { - fun make(throwable: Throwable, action: FallibleUiAction) = when (action) { - is StatusAction.Bookmark -> Bookmark(throwable, action) - is StatusAction.Favourite -> Favourite(throwable, action) - is StatusAction.Reblog -> Reblog(throwable, action) - is StatusAction.VoteInPoll -> VoteInPoll(throwable, action) - is StatusAction.Translate -> TranslateStatus(throwable, action) + fun make(error: PachliError, action: FallibleUiAction) = when (action) { + is StatusAction.Bookmark -> Bookmark(error, action) + is StatusAction.Favourite -> Favourite(error, action) + is StatusAction.Reblog -> Reblog(error, action) + is StatusAction.VoteInPoll -> VoteInPoll(error, action) + is StatusAction.Translate -> TranslateStatus(error, action) } } } @@ -345,41 +343,40 @@ abstract class TimelineViewModel( uiAction.filterIsInstance() .throttleFirst() // avoid double-taps .collect { action -> - try { - when (action) { - is StatusAction.Bookmark -> - timelineCases.bookmark( - action.statusViewData.actionableId, - action.state, - ) - is StatusAction.Favourite -> - timelineCases.favourite( - action.statusViewData.actionableId, - action.state, - ) - is StatusAction.Reblog -> - timelineCases.reblog( - action.statusViewData.actionableId, - action.state, - ) - is StatusAction.VoteInPoll -> - timelineCases.voteInPoll( - action.statusViewData.actionableId, - action.poll.id, - action.choices, - ) - is StatusAction.Translate -> { - timelineCases.translate(action.statusViewData) - } - }.getOrThrow() - // TODO: This should look like the equivalent code in - // NotificationsViewModel when timelineCases returns - // Result<_, _> instead of NetworkResult. - _uiResult.send(Ok(StatusActionSuccess.from(action))) - } catch (e: Exception) { - currentCoroutineContext().ensureActive() - _uiResult.send(Err(UiError.make(e, action))) - } + val result = when (action) { + is StatusAction.Bookmark -> + timelineCases.bookmark( + action.statusViewData.actionableId, + action.state, + ) + + is StatusAction.Favourite -> + timelineCases.favourite( + action.statusViewData.actionableId, + action.state, + ) + + is StatusAction.Reblog -> + timelineCases.reblog( + action.statusViewData.actionableId, + action.state, + ) + + is StatusAction.VoteInPoll -> + timelineCases.voteInPoll( + action.statusViewData.actionableId, + action.poll.id, + action.choices, + ) + + is StatusAction.Translate -> { + timelineCases.translate(action.statusViewData) + } + }.mapEither( + { StatusActionSuccess.from(action) }, + { UiError.make(it, action) }, + ) + _uiResult.send(result) } } diff --git a/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingLinksViewModel.kt b/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingLinksViewModel.kt index c9699ec35a..347f1fdf05 100644 --- a/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingLinksViewModel.kt +++ b/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingLinksViewModel.kt @@ -28,7 +28,7 @@ import app.pachli.core.data.repository.StatusDisplayOptionsRepository import app.pachli.core.network.model.TrendsLink import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.SharedPreferencesRepository -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.mapBoth import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -78,9 +78,9 @@ class TrendingLinksViewModel @AssistedInject constructor( flow { emit(LoadState.Loading) emit( - repository.getTrendingLinks().fold( - { list -> LoadState.Success(list) }, - { throwable -> LoadState.Error(throwable) }, + repository.getTrendingLinks().mapBoth( + { response -> LoadState.Success(response.body) }, + { error -> LoadState.Error(error.throwable) }, ), ) } diff --git a/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingTagsViewModel.kt b/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingTagsViewModel.kt index ddf797ecdf..443f8a544b 100644 --- a/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingTagsViewModel.kt +++ b/app/src/main/java/app/pachli/components/trending/viewmodel/TrendingTagsViewModel.kt @@ -25,7 +25,8 @@ import app.pachli.core.network.model.end import app.pachli.core.network.model.start import app.pachli.core.network.retrofit.MastodonApi import app.pachli.viewdata.TrendingViewData -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import java.io.IOException import javax.inject.Inject @@ -90,8 +91,9 @@ class TrendingTagsViewModel @Inject constructor( val contentFilters = contentFilters.replayCache.last() - mastodonApi.trendingTags(limit = LIMIT_TRENDING_HASHTAGS).fold( - { tagResponse -> + mastodonApi.trendingTags(limit = LIMIT_TRENDING_HASHTAGS) + .onSuccess { + val tagResponse = it.body val firstTag = tagResponse.firstOrNull() _uiState.value = if (firstTag == null) { TrendingTagsUiState(emptyList(), LoadingState.LOADED) @@ -111,16 +113,15 @@ class TrendingTagsViewModel @Inject constructor( val header = TrendingViewData.Header(firstTag.start(), firstTag.end()) TrendingTagsUiState(listOf(header) + tags, LoadingState.LOADED) } - }, - { error -> - Timber.w(error, "failed loading trending tags") - if (error is IOException) { + } + .onFailure { error -> + Timber.w("failed loading trending tags: %s", error) + if (error.throwable is IOException) { _uiState.value = TrendingTagsUiState(emptyList(), LoadingState.ERROR_NETWORK) } else { _uiState.value = TrendingTagsUiState(emptyList(), LoadingState.ERROR_OTHER) } - }, - ) + } } private fun List.toTrendingViewDataTag(): List { diff --git a/app/src/main/java/app/pachli/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/app/pachli/components/viewthread/ViewThreadViewModel.kt index ea41c06e8c..1530bafe87 100644 --- a/app/src/main/java/app/pachli/components/viewthread/ViewThreadViewModel.kt +++ b/app/src/main/java/app/pachli/components/viewthread/ViewThreadViewModel.kt @@ -19,7 +19,6 @@ package app.pachli.components.viewthread import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.pachli.components.timeline.CachedTimelineRepository -import app.pachli.components.timeline.util.ifExpected import app.pachli.core.data.model.StatusViewData import app.pachli.core.data.repository.AccountManager import app.pachli.core.data.repository.Loadable @@ -43,18 +42,18 @@ import app.pachli.core.model.FilterContext import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi +import app.pachli.core.network.retrofit.apiresult.ClientError import app.pachli.network.ContentFilterModel import app.pachli.usecase.TimelineCases -import at.connyduck.calladapter.networkresult.fold -import at.connyduck.calladapter.networkresult.getOrElse -import at.connyduck.calladapter.networkresult.getOrThrow +import com.github.michaelbull.result.get +import com.github.michaelbull.result.getOrElse +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -64,7 +63,6 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import retrofit2.HttpException import timber.log.Timber @HiltViewModel @@ -171,11 +169,11 @@ class ViewThreadViewModel @Inject constructor( } } else { Timber.d("Loaded status from network") - val result = api.status(id).getOrElse { exception -> - _uiState.value = ThreadUiState.Error(exception) + val result = api.status(id).getOrElse { error -> + _uiState.value = ThreadUiState.Error(error.throwable) return@launch } - StatusViewData.fromStatusAndUiState(account, result, isDetailed = true) + StatusViewData.fromStatusAndUiState(account, result.body, isDetailed = true) } _uiState.value = ThreadUiState.LoadingThread( @@ -188,7 +186,7 @@ class ViewThreadViewModel @Inject constructor( // for the status. Ignore errors, the user still has a functioning UI if the fetch // failed. if (timelineStatusWithAccount != null) { - api.status(id).getOrNull()?.let { + api.status(id).get()?.body?.let { detailedStatus = StatusViewData.from( pachliAccountId = account.id, it, @@ -204,54 +202,53 @@ class ViewThreadViewModel @Inject constructor( val contextResult = contextCall.await() - contextResult.fold( - { statusContext -> - val ids = statusContext.ancestors.map { it.id } + statusContext.descendants.map { it.id } - val cachedViewData = repository.getStatusViewData(activeAccount.id, ids) - val cachedTranslations = repository.getStatusTranslations(activeAccount.id, ids) - val ancestors = statusContext.ancestors.map { status -> - val svd = cachedViewData[status.id] - StatusViewData.from( - pachliAccountId = account.id, - status, - isShowingContent = svd?.contentShowing ?: (account.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive), - isExpanded = svd?.expanded ?: account.alwaysOpenSpoiler, - isCollapsed = svd?.contentCollapsed ?: true, - isDetailed = false, - translationState = svd?.translationState ?: TranslationState.SHOW_ORIGINAL, - translation = cachedTranslations[status.id], - ) - }.filterByFilterAction() - val descendants = statusContext.descendants.map { status -> - val svd = cachedViewData[status.id] - StatusViewData.from( - pachliAccountId = account.id, - status, - isShowingContent = svd?.contentShowing ?: (account.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive), - isExpanded = svd?.expanded ?: account.alwaysOpenSpoiler, - isCollapsed = svd?.contentCollapsed ?: true, - isDetailed = false, - translationState = svd?.translationState ?: TranslationState.SHOW_ORIGINAL, - translation = cachedTranslations[status.id], - ) - }.filterByFilterAction() - val statuses = ancestors + detailedStatus + descendants - - _uiState.value = ThreadUiState.Success( - statusViewData = statuses, - detailedStatusPosition = ancestors.size, - revealButton = statuses.getRevealButtonState(), + contextResult.onSuccess { + val statusContext = it.body + val ids = statusContext.ancestors.map { it.id } + statusContext.descendants.map { it.id } + val cachedViewData = repository.getStatusViewData(activeAccount.id, ids) + val cachedTranslations = repository.getStatusTranslations(activeAccount.id, ids) + val ancestors = statusContext.ancestors.map { status -> + val svd = cachedViewData[status.id] + StatusViewData.from( + pachliAccountId = account.id, + status, + isShowingContent = svd?.contentShowing ?: (account.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive), + isExpanded = svd?.expanded ?: account.alwaysOpenSpoiler, + isCollapsed = svd?.contentCollapsed ?: true, + isDetailed = false, + translationState = svd?.translationState ?: TranslationState.SHOW_ORIGINAL, + translation = cachedTranslations[status.id], ) - }, - { throwable -> - _errors.emit(throwable) + }.filterByFilterAction() + val descendants = statusContext.descendants.map { status -> + val svd = cachedViewData[status.id] + StatusViewData.from( + pachliAccountId = account.id, + status, + isShowingContent = svd?.contentShowing ?: (account.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive), + isExpanded = svd?.expanded ?: account.alwaysOpenSpoiler, + isCollapsed = svd?.contentCollapsed ?: true, + isDetailed = false, + translationState = svd?.translationState ?: TranslationState.SHOW_ORIGINAL, + translation = cachedTranslations[status.id], + ) + }.filterByFilterAction() + val statuses = ancestors + detailedStatus + descendants + + _uiState.value = ThreadUiState.Success( + statusViewData = statuses, + detailedStatusPosition = ancestors.size, + revealButton = statuses.getRevealButtonState(), + ) + } + .onFailure { error -> + _errors.emit(error.throwable) _uiState.value = ThreadUiState.Success( statusViewData = listOf(detailedStatus), detailedStatusPosition = 0, revealButton = RevealButtonState.NO_BUTTON, ) - }, - ) + } } } @@ -275,36 +272,21 @@ class ViewThreadViewModel @Inject constructor( } } - fun reblog(reblog: Boolean, status: StatusViewData): Job = viewModelScope.launch { - try { - timelineCases.reblog(status.actionableId, reblog).getOrThrow() - } catch (t: Exception) { - currentCoroutineContext().ensureActive() - ifExpected(t) { - Timber.d(t, "Failed to reblog status: %s", status.actionableId) - } + fun reblog(reblog: Boolean, status: StatusViewData) = viewModelScope.launch { + timelineCases.reblog(status.actionableId, reblog).onFailure { + Timber.d("Failed to reblog status: %s: %s", status.actionableId, it) } } - fun favorite(favorite: Boolean, status: StatusViewData): Job = viewModelScope.launch { - try { - timelineCases.favourite(status.actionableId, favorite).getOrThrow() - } catch (t: Exception) { - currentCoroutineContext().ensureActive() - ifExpected(t) { - Timber.d(t, "Failed to favourite status: %s ", status.actionableId) - } + fun favorite(favorite: Boolean, status: StatusViewData) = viewModelScope.launch { + timelineCases.favourite(status.actionableId, favorite).onFailure { + Timber.d("Failed to favourite status: %s: %s", status.actionableId, it) } } - fun bookmark(bookmark: Boolean, status: StatusViewData): Job = viewModelScope.launch { - try { - timelineCases.bookmark(status.actionableId, bookmark).getOrThrow() - } catch (t: Exception) { - currentCoroutineContext().ensureActive() - ifExpected(t) { - Timber.d(t, "Failed to bookmark status: %s", status.actionableId) - } + fun bookmark(bookmark: Boolean, status: StatusViewData) = viewModelScope.launch { + timelineCases.bookmark(status.actionableId, bookmark).onFailure { + Timber.d("Failed to bookmark status: %s: %s", status.actionableId, it) } } @@ -314,13 +296,8 @@ class ViewThreadViewModel @Inject constructor( status.copy(poll = votedPoll) } - try { - timelineCases.voteInPoll(status.actionableId, poll.id, choices).getOrThrow() - } catch (t: Exception) { - currentCoroutineContext().ensureActive() - ifExpected(t) { - Timber.d(t, "Failed to vote in poll: %s", status.actionableId) - } + timelineCases.voteInPoll(status.actionableId, poll.id, choices).onFailure { + Timber.d("Failed to vote in poll: %s: %s", status.actionableId, it) } } @@ -467,31 +444,29 @@ class ViewThreadViewModel @Inject constructor( fun translate(statusViewData: StatusViewData) { viewModelScope.launch { - repository.translate(statusViewData).fold( - { - val translatedEntity = TranslatedStatusEntity( - serverId = statusViewData.actionableId, - timelineUserId = statusViewData.pachliAccountId, - content = it.content, - spoilerText = it.spoilerText, - poll = it.poll, - attachments = it.attachments, - provider = it.provider, - ) - updateStatusViewData(statusViewData.status.id) { viewData -> - viewData.copy(translation = translatedEntity, translationState = TranslationState.SHOW_TRANSLATION) - } - }, - { + repository.translate(statusViewData).onSuccess { + val body = it.body + val translatedEntity = TranslatedStatusEntity( + serverId = statusViewData.actionableId, + timelineUserId = statusViewData.pachliAccountId, + content = body.content, + spoilerText = body.spoilerText, + poll = body.poll, + attachments = body.attachments, + provider = body.provider, + ) + updateStatusViewData(statusViewData.status.id) { viewData -> + viewData.copy(translation = translatedEntity, translationState = TranslationState.SHOW_TRANSLATION) + } + } + .onFailure { // Mastodon returns 403 if it thinks the original status language is the // same as the user's language, ignoring the actual content of the status // (https://github.com/mastodon/documentation/issues/1330). Nothing useful // to do here so swallow the error - if (it is HttpException && it.code() == 403) return@fold - - _errors.emit(it) - }, - ) + if (it is ClientError && it.exception.code() == 403) return@launch + _errors.emit(it.throwable) + } } } diff --git a/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsViewModel.kt b/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsViewModel.kt index cbc842de68..148c32d4db 100644 --- a/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsViewModel.kt +++ b/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsViewModel.kt @@ -23,7 +23,7 @@ import app.pachli.components.viewthread.edits.PachliTagHandler.Companion.DELETED import app.pachli.components.viewthread.edits.PachliTagHandler.Companion.INSERTED_TEXT_EL import app.pachli.core.network.model.StatusEdit import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.getOrElse +import com.github.michaelbull.result.getOrElse import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -66,9 +66,9 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie viewModelScope.launch { val edits = api.statusEdits(statusId).getOrElse { - _uiState.value = EditsUiState.Error(it) + _uiState.value = EditsUiState.Error(it.throwable) return@launch - } + }.body // `edits` might have fewer than the minimum number of entries because of // https://github.com/mastodon/mastodon/issues/25398. diff --git a/app/src/main/java/app/pachli/fragment/SFragment.kt b/app/src/main/java/app/pachli/fragment/SFragment.kt index 05b9c0c1be..d8d0f55e61 100644 --- a/app/src/main/java/app/pachli/fragment/SFragment.kt +++ b/app/src/main/java/app/pachli/fragment/SFragment.kt @@ -59,12 +59,9 @@ import app.pachli.core.network.model.Status import app.pachli.core.network.parseAsMastodonHtml import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.ui.ClipboardUseCase -import app.pachli.core.ui.extensions.getErrorString import app.pachli.interfaces.StatusActionListener import app.pachli.usecase.TimelineCases import app.pachli.view.showMuteAccountDialog -import at.connyduck.calladapter.networkresult.fold -import at.connyduck.calladapter.networkresult.onFailure import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import com.google.android.material.snackbar.Snackbar @@ -342,8 +339,8 @@ abstract class SFragment : Fragment(), StatusActionListener } R.id.pin -> { lifecycleScope.launch { - timelineCases.pin(status.id, !status.isPinned()).onFailure { e: Throwable -> - val message = e.getErrorString(requireContext()) + timelineCases.pin(status.id, !status.isPinned()).onFailure { e -> + val message = e.fmt(requireContext()) Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show() } } @@ -443,9 +440,8 @@ abstract class SFragment : Fragment(), StatusActionListener .setMessage(R.string.dialog_delete_post_warning) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> lifecycleScope.launch { - val result = timelineCases.delete(viewData.status.id).exceptionOrNull() - if (result != null) { - Timber.w(result, "error deleting status") + timelineCases.delete(viewData.status.id).onFailure { + Timber.w("error deleting status: %s", it) Toast.makeText(context, app.pachli.core.ui.R.string.error_generic, Toast.LENGTH_SHORT).show() } // XXX: Removes the item even if there was an error. This is probably not @@ -468,34 +464,33 @@ abstract class SFragment : Fragment(), StatusActionListener .setMessage(R.string.dialog_redraft_post_warning) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> lifecycleScope.launch { - timelineCases.delete(statusViewData.status.id).fold( - { deletedStatus -> - removeItem(statusViewData) - val sourceStatus = if (deletedStatus.isEmpty()) { - statusViewData.status.toDeletedStatus() - } else { - deletedStatus - } - val composeOptions = ComposeOptions( - content = sourceStatus.text, - inReplyToId = sourceStatus.inReplyToId, - visibility = sourceStatus.visibility, - contentWarning = sourceStatus.spoilerText, - mediaAttachments = sourceStatus.attachments, - sensitive = sourceStatus.sensitive, - modifiedInitialState = true, - language = sourceStatus.language, - poll = sourceStatus.poll?.toNewPoll(sourceStatus.createdAt), - kind = ComposeOptions.ComposeKind.NEW, - ) - startActivity(ComposeActivityIntent(requireContext(), pachliAccountId, composeOptions)) - }, - { error: Throwable? -> - Timber.w(error, "error deleting status") + timelineCases.delete(statusViewData.status.id).onSuccess { + val deletedStatus = it.body + removeItem(statusViewData) + val sourceStatus = if (deletedStatus.isEmpty()) { + statusViewData.status.toDeletedStatus() + } else { + deletedStatus + } + val composeOptions = ComposeOptions( + content = sourceStatus.text, + inReplyToId = sourceStatus.inReplyToId, + visibility = sourceStatus.visibility, + contentWarning = sourceStatus.spoilerText, + mediaAttachments = sourceStatus.attachments, + sensitive = sourceStatus.sensitive, + modifiedInitialState = true, + language = sourceStatus.language, + poll = sourceStatus.poll?.toNewPoll(sourceStatus.createdAt), + kind = ComposeOptions.ComposeKind.NEW, + ) + startActivity(ComposeActivityIntent(requireContext(), pachliAccountId, composeOptions)) + } + .onFailure { + Timber.w("error deleting status: %s", it) Toast.makeText(context, app.pachli.core.ui.R.string.error_generic, Toast.LENGTH_SHORT) .show() - }, - ) + } } } .setNegativeButton(android.R.string.cancel, null) @@ -504,30 +499,29 @@ abstract class SFragment : Fragment(), StatusActionListener private fun editStatus(id: String, status: Status) { lifecycleScope.launch { - mastodonApi.statusSource(id).fold( - { source -> - val composeOptions = ComposeOptions( - content = source.text, - inReplyToId = status.inReplyToId, - visibility = status.visibility, - contentWarning = source.spoilerText, - mediaAttachments = status.attachments, - sensitive = status.sensitive, - language = status.language, - statusId = source.id, - poll = status.poll?.toNewPoll(status.createdAt), - kind = ComposeOptions.ComposeKind.EDIT_POSTED, - ) - startActivity(ComposeActivityIntent(requireContext(), pachliAccountId, composeOptions)) - }, - { + mastodonApi.statusSource(id).onSuccess { + val source = it.body + val composeOptions = ComposeOptions( + content = source.text, + inReplyToId = status.inReplyToId, + visibility = status.visibility, + contentWarning = source.spoilerText, + mediaAttachments = status.attachments, + sensitive = status.sensitive, + language = status.language, + statusId = source.id, + poll = status.poll?.toNewPoll(status.createdAt), + kind = ComposeOptions.ComposeKind.EDIT_POSTED, + ) + startActivity(ComposeActivityIntent(requireContext(), pachliAccountId, composeOptions)) + } + .onFailure { Snackbar.make( requireView(), getString(R.string.error_status_source_load), Snackbar.LENGTH_SHORT, ).show() - }, - ) + } } } diff --git a/app/src/main/java/app/pachli/service/SendStatusService.kt b/app/src/main/java/app/pachli/service/SendStatusService.kt index 4339be8588..cfb92a3bc0 100644 --- a/app/src/main/java/app/pachli/service/SendStatusService.kt +++ b/app/src/main/java/app/pachli/service/SendStatusService.kt @@ -36,7 +36,6 @@ import app.pachli.core.network.model.NewPoll import app.pachli.core.network.model.NewStatus import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess @@ -249,33 +248,33 @@ class SendStatusService : Service() { ) } - sendResult.fold( - { sentStatus -> - statusesToSend.remove(statusId) - // If the status was loaded from a draft, delete the draft and associated media files. - if (statusToSend.draftId != 0) { - draftHelper.deleteDraftAndAttachments(statusToSend.draftId) - } + sendResult.onSuccess { + val sentStatus = it.body + statusesToSend.remove(statusId) + // If the status was loaded from a draft, delete the draft and associated media files. + if (statusToSend.draftId != 0) { + draftHelper.deleteDraftAndAttachments(statusToSend.draftId) + } - mediaUploader.cancelUploadScope(*statusToSend.media.map { it.localId }.toIntArray()) + mediaUploader.cancelUploadScope(*statusToSend.media.map { it.localId }.toIntArray()) - val scheduled = statusToSend.scheduledAt != null + val scheduled = statusToSend.scheduledAt != null - if (scheduled) { - eventHub.dispatch(StatusScheduledEvent) - } else if (!isNew) { - eventHub.dispatch(StatusEditedEvent(statusToSend.statusId!!, sentStatus as Status)) - } else { - eventHub.dispatch(StatusComposedEvent(sentStatus as Status)) - } + if (scheduled) { + eventHub.dispatch(StatusScheduledEvent) + } else if (!isNew) { + eventHub.dispatch(StatusEditedEvent(statusToSend.statusId!!, sentStatus as Status)) + } else { + eventHub.dispatch(StatusComposedEvent(sentStatus as Status)) + } + + notificationManager.cancel(statusId) + } + .onFailure { + Timber.w("failed sending status: %s", it) + failOrRetry(it.throwable, statusId) + } - notificationManager.cancel(statusId) - }, - { throwable -> - Timber.w(throwable, "failed sending status") - failOrRetry(throwable, statusId) - }, - ) stopSelfWhenDone() } } diff --git a/app/src/main/java/app/pachli/usecase/TimelineCases.kt b/app/src/main/java/app/pachli/usecase/TimelineCases.kt index b616da9bd9..ba349660b9 100644 --- a/app/src/main/java/app/pachli/usecase/TimelineCases.kt +++ b/app/src/main/java/app/pachli/usecase/TimelineCases.kt @@ -34,10 +34,9 @@ import app.pachli.core.network.model.Relationship import app.pachli.core.network.model.Status import app.pachli.core.network.model.Translation import app.pachli.core.network.retrofit.MastodonApi -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 app.pachli.core.network.retrofit.apiresult.ApiResult +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import javax.inject.Inject import timber.log.Timber @@ -47,7 +46,7 @@ class TimelineCases @Inject constructor( private val cachedTimelineRepository: CachedTimelineRepository, ) { - suspend fun reblog(statusId: String, reblog: Boolean): NetworkResult { + suspend fun reblog(statusId: String, reblog: Boolean): ApiResult { return if (reblog) { mastodonApi.reblogStatus(statusId) } else { @@ -57,7 +56,7 @@ class TimelineCases @Inject constructor( } } - suspend fun favourite(statusId: String, favourite: Boolean): NetworkResult { + suspend fun favourite(statusId: String, favourite: Boolean): ApiResult { return if (favourite) { mastodonApi.favouriteStatus(statusId) } else { @@ -67,7 +66,7 @@ class TimelineCases @Inject constructor( } } - suspend fun bookmark(statusId: String, bookmark: Boolean): NetworkResult { + suspend fun bookmark(statusId: String, bookmark: Boolean): ApiResult { return if (bookmark) { mastodonApi.bookmarkStatus(statusId) } else { @@ -77,7 +76,7 @@ class TimelineCases @Inject constructor( } } - suspend fun muteConversation(statusId: String, mute: Boolean): NetworkResult { + suspend fun muteConversation(statusId: String, mute: Boolean): ApiResult { return if (mute) { mastodonApi.muteConversation(statusId) } else { @@ -105,45 +104,37 @@ class TimelineCases @Inject constructor( } } - suspend fun delete(statusId: String): NetworkResult { + suspend fun delete(statusId: String): ApiResult { return mastodonApi.deleteStatus(statusId) .onSuccess { eventHub.dispatch(StatusDeletedEvent(statusId)) } - .onFailure { Timber.w(it, "Failed to delete status") } + .onFailure { Timber.w("Failed to delete status: %s", it) } } - suspend fun pin(statusId: String, pin: Boolean): NetworkResult { + suspend fun pin(statusId: String, pin: Boolean): ApiResult { return if (pin) { mastodonApi.pinStatus(statusId) } else { mastodonApi.unpinStatus(statusId) - }.fold({ status -> + }.onSuccess { eventHub.dispatch(PinEvent(statusId, pin)) - NetworkResult.success(status) - }, { e -> - Timber.w(e, "Failed to change pin state") - NetworkResult.failure(e) - }) - } - - suspend fun voteInPoll(statusId: String, pollId: String, choices: List): NetworkResult { - if (choices.isEmpty()) { - return NetworkResult.failure(IllegalStateException()) } + } - return mastodonApi.voteInPoll(pollId, choices).onSuccess { poll -> - eventHub.dispatch(PollVoteEvent(statusId, poll)) + suspend fun voteInPoll(statusId: String, pollId: String, choices: List): ApiResult { + return mastodonApi.voteInPoll(pollId, choices).onSuccess { + eventHub.dispatch(PollVoteEvent(statusId, it.body)) } } - suspend fun acceptFollowRequest(accountId: String): NetworkResult { + suspend fun acceptFollowRequest(accountId: String): ApiResult { return mastodonApi.authorizeFollowRequest(accountId) } - suspend fun rejectFollowRequest(accountId: String): NetworkResult { + suspend fun rejectFollowRequest(accountId: String): ApiResult { return mastodonApi.rejectFollowRequest(accountId) } - suspend fun translate(statusViewData: StatusViewData): NetworkResult { + suspend fun translate(statusViewData: StatusViewData): ApiResult { return cachedTimelineRepository.translate(statusViewData) } diff --git a/app/src/main/java/app/pachli/viewmodel/EditProfileViewModel.kt b/app/src/main/java/app/pachli/viewmodel/EditProfileViewModel.kt index 1b272987a3..bbf516b1e0 100644 --- a/app/src/main/java/app/pachli/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/app/pachli/viewmodel/EditProfileViewModel.kt @@ -34,7 +34,6 @@ import app.pachli.util.Error import app.pachli.util.Loading import app.pachli.util.Resource import app.pachli.util.Success -import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel @@ -150,16 +149,14 @@ class EditProfileViewModel @Inject constructor( diff.field3?.second?.toRequestBody(MultipartBody.FORM), diff.field4?.first?.toRequestBody(MultipartBody.FORM), diff.field4?.second?.toRequestBody(MultipartBody.FORM), - ).fold( - { newAccountData -> - accountManager.updateAccount(pachliAccountId, newAccountData) - saveData.postValue(Success()) - eventHub.dispatch(ProfileEditedEvent(newAccountData)) - }, - { throwable -> - saveData.postValue(Error(cause = throwable)) - }, - ) + ).onSuccess { + val newAccountData = it.body + accountManager.updateAccount(pachliAccountId, newAccountData) + saveData.postValue(Success()) + eventHub.dispatch(ProfileEditedEvent(newAccountData)) + }.onFailure { + saveData.postValue(Error(cause = it.throwable)) + } } } diff --git a/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt b/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt index 6a0c923a83..870a647b92 100644 --- a/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt +++ b/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt @@ -41,7 +41,6 @@ import app.pachli.core.testing.failure import app.pachli.core.testing.rules.MainCoroutineRule import app.pachli.core.testing.rules.lazyActivityScenarioRule import app.pachli.core.testing.success -import at.connyduck.calladapter.networkresult.NetworkResult import com.github.michaelbull.result.andThen import com.github.michaelbull.result.get import com.github.michaelbull.result.onSuccess @@ -153,7 +152,7 @@ class ComposeActivityTest { } } } - onBlocking { search(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn NetworkResult.success( + onBlocking { search(any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn success( SearchResult(emptyList(), emptyList(), emptyList()), ) onBlocking { getLists() } doReturn success(emptyList()) diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt index a507a83d01..5c5c6e2964 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestBase.kt @@ -50,8 +50,6 @@ import javax.inject.Inject import kotlin.properties.Delegates import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest -import okhttp3.ResponseBody -import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith @@ -62,8 +60,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.reset import org.mockito.kotlin.stub import org.robolectric.annotation.Config -import retrofit2.HttpException -import retrofit2.Response open class PachliHiltApplication : PachliApplication() @@ -109,15 +105,6 @@ abstract class NotificationsViewModelTestBase { private lateinit var accountPreferenceDataStore: AccountPreferenceDataStore - /** Empty success response, for API calls that return one */ - protected var emptySuccess: Response = Response.success("".toResponseBody()) - - /** Empty error response, for API calls that return one */ - protected var emptyError: Response = Response.error(404, "".toResponseBody()) - - /** Exception to throw when testing errors */ - protected val httpException = HttpException(emptyError) - private val account = Account( id = "1", localUsername = "username", diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestClearNotifications.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestClearNotifications.kt index cb0639d63a..0ab8306f12 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestClearNotifications.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestClearNotifications.kt @@ -18,6 +18,8 @@ package app.pachli.components.notifications import app.cash.turbine.test +import app.pachli.core.testing.failure +import app.pachli.core.testing.success import com.github.michaelbull.result.getError import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest @@ -40,7 +42,7 @@ class NotificationsViewModelTestClearNotifications : NotificationsViewModelTestB @Test fun `clearing notifications succeeds`() = runTest { // Given - mastodonApi.stub { onBlocking { clearNotifications() } doReturn emptySuccess } + mastodonApi.stub { onBlocking { clearNotifications() } doReturn success(Unit) } // When viewModel.accept(FallibleUiAction.ClearNotifications) @@ -52,7 +54,7 @@ class NotificationsViewModelTestClearNotifications : NotificationsViewModelTestB @Test fun `clearing notifications fails && emits UiError`() = runTest { // Given - notificationsRepository.stub { onBlocking { clearNotifications() } doReturn emptyError } + notificationsRepository.stub { onBlocking { clearNotifications() } doReturn failure() } viewModel.uiResult.test { // When diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationFilterAction.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationFilterAction.kt index f6c3ff4e79..6bb775bc2f 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationFilterAction.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestNotificationFilterAction.kt @@ -19,7 +19,8 @@ package app.pachli.components.notifications import app.cash.turbine.test import app.pachli.core.network.model.Relationship -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.testing.failure +import app.pachli.core.testing.success import com.github.michaelbull.result.get import com.github.michaelbull.result.getError import com.google.common.truth.Truth.assertThat @@ -29,7 +30,6 @@ import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doThrow import org.mockito.kotlin.stub import org.mockito.kotlin.verify @@ -70,7 +70,7 @@ class NotificationsViewModelTestNotificationFilterAction : NotificationsViewMode fun `accepting follow request succeeds && emits UiSuccess`() = runTest { // Given timelineCases.stub { - onBlocking { acceptFollowRequest(any()) } doReturn NetworkResult.success(relationship) + onBlocking { acceptFollowRequest(any()) } doReturn success(relationship) } viewModel.uiResult.test { @@ -92,7 +92,7 @@ class NotificationsViewModelTestNotificationFilterAction : NotificationsViewMode @Test fun `accepting follow request fails && emits UiError`() = runTest { // Given - timelineCases.stub { onBlocking { acceptFollowRequest(any()) } doThrow httpException } + timelineCases.stub { onBlocking { acceptFollowRequest(any()) } doReturn failure() } viewModel.uiResult.test { // When @@ -107,7 +107,7 @@ class NotificationsViewModelTestNotificationFilterAction : NotificationsViewMode @Test fun `rejecting follow request succeeds && emits UiSuccess`() = runTest { // Given - timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn NetworkResult.success(relationship) } + timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn success(relationship) } viewModel.uiResult.test { // When @@ -128,7 +128,7 @@ class NotificationsViewModelTestNotificationFilterAction : NotificationsViewMode @Test fun `rejecting follow request fails && emits UiError`() = runTest { // Given - timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doThrow httpException } + timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn failure() } viewModel.uiResult.test { // When diff --git a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusFilterAction.kt b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusFilterAction.kt index 67a31bf6ce..6e76d388cf 100644 --- a/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusFilterAction.kt +++ b/app/src/test/java/app/pachli/components/notifications/NotificationsViewModelTestStatusFilterAction.kt @@ -22,6 +22,8 @@ import app.pachli.ContentFilterV1Test.Companion.mockStatus import app.pachli.core.data.model.StatusViewData import app.pachli.core.data.repository.notifications.StatusActionError import app.pachli.core.database.model.TranslationState +import app.pachli.core.network.retrofit.apiresult.ApiResult +import app.pachli.core.testing.failure import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.get @@ -54,6 +56,9 @@ class NotificationsViewModelTestStatusFilterAction : NotificationsViewModelTestB translationState = TranslationState.SHOW_ORIGINAL, ) + /** Empty error response, for API calls that return one */ + private var emptyError = failure>().getError()!! + /** Action to bookmark a status */ private val bookmarkAction = StatusAction.Bookmark(true, statusViewData) @@ -89,7 +94,7 @@ class NotificationsViewModelTestStatusFilterAction : NotificationsViewModelTestB // Given notificationsRepository.stub { onBlocking { bookmark(any(), any(), any()) } doReturn Err( - StatusActionError.Bookmark(httpException), + StatusActionError.Bookmark(emptyError), ) } @@ -123,7 +128,7 @@ class NotificationsViewModelTestStatusFilterAction : NotificationsViewModelTestB // Given notificationsRepository.stub { onBlocking { favourite(any(), any(), any()) } doReturn Err( - StatusActionError.Favourite(httpException), + StatusActionError.Favourite(emptyError), ) } @@ -157,7 +162,7 @@ class NotificationsViewModelTestStatusFilterAction : NotificationsViewModelTestB // Given notificationsRepository.stub { onBlocking { reblog(any(), any(), any()) } doReturn Err( - StatusActionError.Reblog(httpException), + StatusActionError.Reblog(emptyError), ) } @@ -191,7 +196,7 @@ class NotificationsViewModelTestStatusFilterAction : NotificationsViewModelTestB // Given notificationsRepository.stub { onBlocking { voteInPoll(any(), any(), any(), any()) } doReturn Err( - StatusActionError.VoteInPoll(httpException), + StatusActionError.VoteInPoll(emptyError), ) } diff --git a/app/src/test/java/app/pachli/components/timeline/CachedTimelineRemoteMediatorTest.kt b/app/src/test/java/app/pachli/components/timeline/CachedTimelineRemoteMediatorTest.kt index b19a8b8d94..1a0df371cb 100644 --- a/app/src/test/java/app/pachli/components/timeline/CachedTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/app/pachli/components/timeline/CachedTimelineRemoteMediatorTest.kt @@ -25,16 +25,15 @@ import app.pachli.core.network.json.DefaultIfNull import app.pachli.core.network.json.Guarded import app.pachli.core.network.json.InstantJsonAdapter import app.pachli.core.network.json.LenientRfc3339DateJsonAdapter +import app.pachli.core.testing.failure +import app.pachli.core.testing.success import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi -import java.io.IOException import java.time.Instant import java.util.Date import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest -import okhttp3.Headers -import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -43,10 +42,8 @@ import org.junit.Test 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 retrofit2.HttpException -import retrofit2.Response @RunWith(AndroidJUnit4::class) class CachedTimelineRemoteMediatorTest { @@ -98,7 +95,7 @@ class CachedTimelineRemoteMediatorTest { fun `should return error when network call returns error code`() { val remoteMediator = CachedTimelineRemoteMediator( mastodonApi = mock { - onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody()) + onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn failure(code = 500) }, pachliAccountId = activeAccount.id, transactionProvider = transactionProvider, @@ -119,7 +116,7 @@ class CachedTimelineRemoteMediatorTest { fun `should return error when network call fails`() { val remoteMediator = CachedTimelineRemoteMediator( mastodonApi = mock { - onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException() + onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn failure() }, pachliAccountId = activeAccount.id, transactionProvider = transactionProvider, @@ -131,7 +128,7 @@ class CachedTimelineRemoteMediatorTest { val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) } assertTrue(result is RemoteMediator.MediatorResult.Error) - assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException) + assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException) } @Test @@ -169,7 +166,7 @@ class CachedTimelineRemoteMediatorTest { fun `should not try to refresh already cached statuses when db is empty`() { val remoteMediator = CachedTimelineRemoteMediator( mastodonApi = mock { - onBlocking { homeTimeline(limit = 20) } doReturn Response.success( + onBlocking { homeTimeline(limit = 20) } doReturn success( listOf( mockStatus("5"), mockStatus("4"), @@ -222,7 +219,7 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( mastodonApi = mock { - onBlocking { homeTimeline(limit = 20) } doReturn Response.success( + onBlocking { homeTimeline(limit = 20) } doReturn success( listOf( mockStatus("3"), mockStatus("1"), @@ -277,15 +274,14 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( mastodonApi = mock { - onBlocking { homeTimeline(maxId = "5", limit = 20) } doReturn Response.success( + onBlocking { homeTimeline(maxId = "5", limit = 20) } doReturn success( listOf( mockStatus("3"), mockStatus("2"), mockStatus("1"), ), - Headers.Builder().add( - "Link: ; rel=\"prev\", ; rel=\"next\"", - ).build(), + headers = arrayOf("Link", "; rel=\"prev\", ; rel=\"next\""), + ) }, pachliAccountId = activeAccount.id, diff --git a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt index 561753febc..8a521ac2d3 100644 --- a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt +++ b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestBase.kt @@ -48,8 +48,6 @@ import java.time.Instant import java.util.Date import javax.inject.Inject import kotlinx.coroutines.test.runTest -import okhttp3.ResponseBody -import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith @@ -60,8 +58,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.reset import org.mockito.kotlin.stub import org.robolectric.annotation.Config -import retrofit2.HttpException -import retrofit2.Response open class PachliHiltApplication : PachliApplication() @@ -107,12 +103,6 @@ abstract class CachedTimelineViewModelTestBase { private val eventHub = EventHub() - /** Empty error response, for API calls that return one */ - private var emptyError: Response = Response.error(404, "".toResponseBody()) - - /** Exception to throw when testing errors */ - protected val httpException = HttpException(emptyError) - val account = Account( id = "1", localUsername = "username", diff --git a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusFilterAction.kt b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusFilterAction.kt index 7b5fc81467..a261936e29 100644 --- a/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusFilterAction.kt +++ b/app/src/test/java/app/pachli/components/timeline/CachedTimelineViewModelTestStatusFilterAction.kt @@ -24,7 +24,8 @@ import app.pachli.components.timeline.viewmodel.StatusActionSuccess import app.pachli.components.timeline.viewmodel.UiError import app.pachli.core.data.model.StatusViewData import app.pachli.core.database.model.TranslationState -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.testing.failure +import app.pachli.core.testing.success import com.github.michaelbull.result.get import com.github.michaelbull.result.getError import com.google.common.truth.Truth.assertThat @@ -34,7 +35,6 @@ import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doThrow import org.mockito.kotlin.stub import org.mockito.kotlin.verify @@ -83,7 +83,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes @Test fun `bookmark succeeds && emits UiSuccess`() = runTest { // Given - timelineCases.stub { onBlocking { bookmark(any(), any()) } doReturn NetworkResult.success(status) } + timelineCases.stub { onBlocking { bookmark(any(), any()) } doReturn success(status) } viewModel.uiResult.test { // When @@ -103,7 +103,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes @Test fun `bookmark fails && emits UiError`() = runTest { // Given - timelineCases.stub { onBlocking { bookmark(any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { bookmark(any(), any()) } doReturn failure() } viewModel.uiResult.test { // When @@ -119,7 +119,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes fun `favourite succeeds && emits UiSuccess`() = runTest { // Given timelineCases.stub { - onBlocking { favourite(any(), any()) } doReturn NetworkResult.success(status) + onBlocking { favourite(any(), any()) } doReturn success(status) } viewModel.uiResult.test { @@ -140,7 +140,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes @Test fun `favourite fails && emits UiError`() = runTest { // Given - timelineCases.stub { onBlocking { favourite(any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { favourite(any(), any()) } doReturn failure() } viewModel.uiResult.test { // When @@ -155,7 +155,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes @Test fun `reblog succeeds && emits UiSuccess`() = runTest { // Given - timelineCases.stub { onBlocking { reblog(any(), any()) } doReturn NetworkResult.success(status) } + timelineCases.stub { onBlocking { reblog(any(), any()) } doReturn success(status) } viewModel.uiResult.test { // When @@ -175,7 +175,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes @Test fun `reblog fails && emits UiError`() = runTest { // Given - timelineCases.stub { onBlocking { reblog(any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { reblog(any(), any()) } doReturn failure() } viewModel.uiResult.test { // When @@ -191,7 +191,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes fun `voteinpoll succeeds && emits UiSuccess`() = runTest { // Given timelineCases.stub { - onBlocking { voteInPoll(any(), any(), any()) } doReturn NetworkResult.success(status.poll!!) + onBlocking { voteInPoll(any(), any(), any()) } doReturn success(status.poll!!) } viewModel.uiResult.test { @@ -215,7 +215,7 @@ class CachedTimelineViewModelTestStatusFilterAction : CachedTimelineViewModelTes @Test fun `voteinpoll fails && emits UiError`() = runTest { // Given - timelineCases.stub { onBlocking { voteInPoll(any(), any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { voteInPoll(any(), any(), any()) } doReturn failure() } viewModel.uiResult.test { // When diff --git a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineRemoteMediatorTest.kt index 560ae6caef..7475109a77 100644 --- a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineRemoteMediatorTest.kt @@ -31,10 +31,10 @@ import app.pachli.components.timeline.viewmodel.PageCache import app.pachli.core.database.model.AccountEntity import app.pachli.core.model.Timeline import app.pachli.core.network.model.Status +import app.pachli.core.testing.failure +import app.pachli.core.testing.success import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest -import okhttp3.Headers -import okhttp3.ResponseBody.Companion.toResponseBody import okio.IOException import org.junit.Before import org.junit.Test @@ -45,7 +45,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.robolectric.annotation.Config import retrofit2.HttpException -import retrofit2.Response @Config(sdk = [29]) @RunWith(AndroidJUnit4::class) @@ -71,7 +70,7 @@ class NetworkTimelineRemoteMediatorTest { fun `should return error when network call returns error code`() = runTest { // Given val remoteMediator = NetworkTimelineRemoteMediator( - api = mock(defaultAnswer = { Response.error(500, "".toResponseBody()) }), + api = mock(defaultAnswer = { failure(code = 500) }), activeAccount = activeAccount, factory = pagingSourceFactory, pageCache = PageCache(), @@ -114,9 +113,9 @@ class NetworkTimelineRemoteMediatorTest { val pages = PageCache() val remoteMediator = NetworkTimelineRemoteMediator( api = mock { - onBlocking { homeTimeline(maxId = anyOrNull(), minId = anyOrNull(), limit = anyOrNull(), sinceId = anyOrNull()) } doReturn Response.success( + onBlocking { homeTimeline(maxId = anyOrNull(), minId = anyOrNull(), limit = anyOrNull(), sinceId = anyOrNull()) } doReturn success( listOf(mockStatus("7"), mockStatus("6"), mockStatus("5")), - Headers.headersOf( + headers = arrayOf( "Link", "; rel=\"next\", ; rel=\"prev\"", ), @@ -178,9 +177,9 @@ class NetworkTimelineRemoteMediatorTest { val remoteMediator = NetworkTimelineRemoteMediator( api = mock { - onBlocking { homeTimeline(maxId = anyOrNull(), minId = anyOrNull(), limit = anyOrNull(), sinceId = anyOrNull()) } doReturn Response.success( + onBlocking { homeTimeline(maxId = anyOrNull(), minId = anyOrNull(), limit = anyOrNull(), sinceId = anyOrNull()) } doReturn success( listOf(mockStatus("10"), mockStatus("9"), mockStatus("8")), - Headers.headersOf( + headers = arrayOf( "Link", "; rel=\"next\", ; rel=\"prev\"", ), @@ -250,9 +249,9 @@ class NetworkTimelineRemoteMediatorTest { val remoteMediator = NetworkTimelineRemoteMediator( api = mock { - onBlocking { homeTimeline(maxId = anyOrNull(), minId = anyOrNull(), limit = anyOrNull(), sinceId = anyOrNull()) } doReturn Response.success( + onBlocking { homeTimeline(maxId = anyOrNull(), minId = anyOrNull(), limit = anyOrNull(), sinceId = anyOrNull()) } doReturn success( listOf(mockStatus("4"), mockStatus("3"), mockStatus("2")), - Headers.headersOf( + headers = arrayOf( "Link", "; rel=\"next\", ; rel=\"prev\"", ), diff --git a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt index 03a19031cf..29b21729cd 100644 --- a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt +++ b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestBase.kt @@ -46,8 +46,6 @@ import java.time.Instant import java.util.Date import javax.inject.Inject import kotlinx.coroutines.test.runTest -import okhttp3.ResponseBody -import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith @@ -58,8 +56,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.reset import org.mockito.kotlin.stub import org.robolectric.annotation.Config -import retrofit2.HttpException -import retrofit2.Response @HiltAndroidTest @Config(application = HiltTestApplication_Application::class) @@ -97,12 +93,6 @@ abstract class NetworkTimelineViewModelTestBase { private val eventHub = EventHub() - /** Empty error response, for API calls that return one */ - private var emptyError: Response = Response.error(404, "".toResponseBody()) - - /** Exception to throw when testing errors */ - protected val httpException = HttpException(emptyError) - private val account = Account( id = "1", localUsername = "username", diff --git a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusFilterAction.kt b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusFilterAction.kt index 6ea9bfb367..aa200efcf0 100644 --- a/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusFilterAction.kt +++ b/app/src/test/java/app/pachli/components/timeline/NetworkTimelineViewModelTestStatusFilterAction.kt @@ -25,7 +25,8 @@ import app.pachli.components.timeline.viewmodel.UiError import app.pachli.components.timeline.viewmodel.UiSuccess import app.pachli.core.data.model.StatusViewData import app.pachli.core.database.model.TranslationState -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.testing.failure +import app.pachli.core.testing.success import com.github.michaelbull.result.get import com.github.michaelbull.result.getError import com.google.common.truth.Truth.assertThat @@ -35,7 +36,6 @@ import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doThrow import org.mockito.kotlin.stub import org.mockito.kotlin.verify @@ -84,7 +84,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT @Test fun `bookmark succeeds && emits Ok uiResult`() = runTest { // Given - timelineCases.stub { onBlocking { bookmark(any(), any()) } doReturn NetworkResult.success(status) } + timelineCases.stub { onBlocking { bookmark(any(), any()) } doReturn success(status) } viewModel.uiResult.test { // When @@ -104,7 +104,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT @Test fun `bookmark fails && emits Err uiResult`() = runTest { // Given - timelineCases.stub { onBlocking { bookmark(any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { bookmark(any(), any()) } doReturn failure() } viewModel.uiResult.test { // When @@ -120,7 +120,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT fun `favourite succeeds && emits Ok uiResult`() = runTest { // Given timelineCases.stub { - onBlocking { favourite(any(), any()) } doReturn NetworkResult.success(status) + onBlocking { favourite(any(), any()) } doReturn success(status) } viewModel.uiResult.test { @@ -141,7 +141,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT @Test fun `favourite fails && emits Err uiResult`() = runTest { // Given - timelineCases.stub { onBlocking { favourite(any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { favourite(any(), any()) } doReturn failure() } viewModel.uiResult.test { // When @@ -156,7 +156,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT @Test fun `reblog succeeds && emits Ok uiResult`() = runTest { // Given - timelineCases.stub { onBlocking { reblog(any(), any()) } doReturn NetworkResult.success(status) } + timelineCases.stub { onBlocking { reblog(any(), any()) } doReturn success(status) } viewModel.uiResult.test { // When @@ -176,7 +176,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT @Test fun `reblog fails && emits Err uiResult`() = runTest { // Given - timelineCases.stub { onBlocking { reblog(any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { reblog(any(), any()) } doReturn failure() } viewModel.uiResult.test { // When @@ -192,7 +192,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT fun `voteinpoll succeeds && emits Ok uiResult`() = runTest { // Given timelineCases.stub { - onBlocking { voteInPoll(any(), any(), any()) } doReturn NetworkResult.success(status.poll!!) + onBlocking { voteInPoll(any(), any(), any()) } doReturn success(status.poll!!) } viewModel.uiResult.test { @@ -216,7 +216,7 @@ class NetworkTimelineViewModelTestStatusFilterAction : NetworkTimelineViewModelT @Test fun `voteinpoll fails && emits Err uiResult`() = runTest { // Given - timelineCases.stub { onBlocking { voteInPoll(any(), any(), any()) } doThrow httpException } + timelineCases.stub { onBlocking { voteInPoll(any(), any(), any()) } doReturn failure() } viewModel.uiResult.test { // When diff --git a/app/src/test/java/app/pachli/components/viewthread/ViewThreadViewModelTest.kt b/app/src/test/java/app/pachli/components/viewthread/ViewThreadViewModelTest.kt index 46d36634a4..624b974ec4 100644 --- a/app/src/test/java/app/pachli/components/viewthread/ViewThreadViewModelTest.kt +++ b/app/src/test/java/app/pachli/components/viewthread/ViewThreadViewModelTest.kt @@ -26,14 +26,12 @@ import app.pachli.core.testing.failure import app.pachli.core.testing.rules.MainCoroutineRule import app.pachli.core.testing.success import app.pachli.usecase.TimelineCases -import at.connyduck.calladapter.networkresult.NetworkResult import com.github.michaelbull.result.andThen import com.github.michaelbull.result.onSuccess import com.squareup.moshi.Moshi import dagger.hilt.android.testing.CustomTestApplication import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import java.io.IOException import java.time.Instant import java.util.Date import javax.inject.Inject @@ -209,8 +207,8 @@ class ViewThreadViewModelTest { @Test fun `should emit status even if context fails to load`() = runTest { mastodonApi.stub { - onBlocking { status(threadId) } doReturn NetworkResult.success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1")) - onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException()) + onBlocking { status(threadId) } doReturn success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1")) + onBlocking { statusContext(threadId) } doReturn failure() } viewModel.uiState.test { @@ -242,8 +240,8 @@ class ViewThreadViewModelTest { @Test fun `should emit error when status and context fail to load`() = runTest { mastodonApi.stub { - onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException()) - onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException()) + onBlocking { status(threadId) } doReturn failure() + onBlocking { statusContext(threadId) } doReturn failure() } viewModel.uiState.test { @@ -264,8 +262,8 @@ class ViewThreadViewModelTest { @Test fun `should emit error when status fails to load`() = runTest { mastodonApi.stub { - onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException()) - onBlocking { statusContext(threadId) } doReturn NetworkResult.success( + onBlocking { status(threadId) } doReturn failure() + onBlocking { statusContext(threadId) } doReturn success( StatusContext( ancestors = listOf(mockStatus(id = "1")), descendants = listOf(mockStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1")), @@ -588,8 +586,8 @@ class ViewThreadViewModelTest { private fun mockSuccessResponses() { mastodonApi.stub { - onBlocking { status(threadId) } doReturn NetworkResult.success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1", spoilerText = "Test")) - onBlocking { statusContext(threadId) } doReturn NetworkResult.success( + onBlocking { status(threadId) } doReturn success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1", spoilerText = "Test")) + onBlocking { statusContext(threadId) } doReturn success( StatusContext( ancestors = listOf(mockStatus(id = "1", spoilerText = "Test")), descendants = listOf(mockStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")), diff --git a/app/src/test/java/app/pachli/usecase/TimelineCasesTest.kt b/app/src/test/java/app/pachli/usecase/TimelineCasesTest.kt index 9e1d976f91..30d0aa9a61 100644 --- a/app/src/test/java/app/pachli/usecase/TimelineCasesTest.kt +++ b/app/src/test/java/app/pachli/usecase/TimelineCasesTest.kt @@ -8,11 +8,11 @@ import app.pachli.core.eventhub.PinEvent import app.pachli.core.network.extensions.getServerErrorMessage import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.testing.failure +import app.pachli.core.testing.success +import com.github.michaelbull.result.getErrorOr import java.util.Date import kotlinx.coroutines.runBlocking -import okhttp3.Protocol -import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -20,8 +20,6 @@ import org.junit.runner.RunWith import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.stub -import retrofit2.HttpException -import retrofit2.Response @RunWith(AndroidJUnit4::class) class TimelineCasesTest { @@ -44,7 +42,7 @@ class TimelineCasesTest { @Test fun `pin success emits PinEvent`() { api.stub { - onBlocking { pinStatus(statusId) } doReturn NetworkResult.success(mockStatus(pinned = true)) + onBlocking { pinStatus(statusId) } doReturn success(mockStatus(pinned = true)) } runBlocking { @@ -58,25 +56,15 @@ class TimelineCasesTest { @Test fun `pin failure with server error returns failure with server message`() { api.stub { - onBlocking { pinStatus(statusId) } doReturn NetworkResult.failure( - HttpException( - Response.error( - "{\"error\":\"Validation Failed: You have already pinned the maximum number of toots\"}".toResponseBody(), - okhttp3.Response.Builder() - .request(okhttp3.Request.Builder().url("http://localhost/").build()) - .protocol(Protocol.HTTP_1_1) - .addHeader("content-type", "application/json") - .code(422) - .message("") - .build(), - ), - ), + onBlocking { pinStatus(statusId) } doReturn failure( + code = 422, + responseBody = "{\"error\":\"Validation Failed: You have already pinned the maximum number of toots\"}", ) } runBlocking { assertEquals( "Validation Failed: You have already pinned the maximum number of toots", - timelineCases.pin(statusId, true).exceptionOrNull()?.getServerErrorMessage(), + timelineCases.pin(statusId, true).getErrorOr(null)?.throwable?.getServerErrorMessage(), ) } } diff --git a/app/src/testFdroid/kotlin/app/pachli/updatecheck/UpdateCheckTest.kt b/app/src/testFdroid/kotlin/app/pachli/updatecheck/UpdateCheckTest.kt index 8f87345bd2..e73183d8a4 100644 --- a/app/src/testFdroid/kotlin/app/pachli/updatecheck/UpdateCheckTest.kt +++ b/app/src/testFdroid/kotlin/app/pachli/updatecheck/UpdateCheckTest.kt @@ -19,8 +19,9 @@ package app.pachli.updatecheck import androidx.test.ext.junit.runners.AndroidJUnit4 import app.pachli.core.preferences.SharedPreferencesRepository +import app.pachli.core.testing.failure import app.pachli.core.testing.fakes.InMemorySharedPreferences -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.testing.success import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -50,7 +51,7 @@ class UpdateCheckTest { @Test fun `remoteFetchLatestVersionCode returns null on network error`() = runTest { fdroidService.stub { - onBlocking { getPackage(any()) } doReturn NetworkResult.failure(Exception()) + onBlocking { getPackage(any()) } doReturn failure() } assertThat(updateCheck.remoteFetchLatestVersionCode()).isNull() @@ -59,7 +60,7 @@ class UpdateCheckTest { @Test fun `remoteFetchLatestVersionCode returns suggestedVersionCode if in packages`() = runTest { fdroidService.stub { - onBlocking { getPackage(any()) } doReturn NetworkResult.success( + onBlocking { getPackage(any()) } doReturn success( FdroidPackage( packageName = "app.pachli", suggestedVersionCode = 3, @@ -79,7 +80,7 @@ class UpdateCheckTest { @Test fun `remoteFetchLatestVersionCode returns greatest code if suggestedVersionCode is missing`() = runTest { fdroidService.stub { - onBlocking { getPackage(any()) } doReturn NetworkResult.success( + onBlocking { getPackage(any()) } doReturn success( FdroidPackage( packageName = "app.pachli", suggestedVersionCode = 3, diff --git a/core/activity/src/main/kotlin/app/pachli/core/activity/BottomSheetActivity.kt b/core/activity/src/main/kotlin/app/pachli/core/activity/BottomSheetActivity.kt index 58dc71888e..dfa261f691 100644 --- a/core/activity/src/main/kotlin/app/pachli/core/activity/BottomSheetActivity.kt +++ b/core/activity/src/main/kotlin/app/pachli/core/activity/BottomSheetActivity.kt @@ -30,7 +30,8 @@ import app.pachli.core.activity.extensions.startActivityWithTransition import app.pachli.core.navigation.AccountActivityIntent import app.pachli.core.navigation.ViewThreadActivityIntent import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.fold +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import com.google.android.material.bottomsheet.BottomSheetBehavior import java.net.URI import java.net.URISyntaxException @@ -80,33 +81,30 @@ abstract class BottomSheetActivity : BaseActivity() { 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(pachliAccountId, it.id, it.url) - return@fold - } + mastodonApi.search(query = url, resolve = true).onSuccess { searchResult -> + val (accounts, statuses) = searchResult.body + if (getCancelSearchRequested(url)) return@onSuccess + onEndSearch(url) + + statuses.firstOrNull()?.let { + viewThread(pachliAccountId, it.id, it.url) + return@onSuccess + } - // 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(pachliAccountId, it.id) - 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(pachliAccountId, it.id) + return@onSuccess + } + performUrlFallbackAction(url, lookupFallbackBehavior) + }.onFailure { + if (!getCancelSearchRequested(url)) { + onEndSearch(url) performUrlFallbackAction(url, lookupFallbackBehavior) - }, - { - if (!getCancelSearchRequested(url)) { - onEndSearch(url) - performUrlFallbackAction(url, lookupFallbackBehavior) - } - }, - ) + } + } } } diff --git a/core/activity/src/test/kotlin/app/pachli/core/activity/BottomSheetActivityTest.kt b/core/activity/src/test/kotlin/app/pachli/core/activity/BottomSheetActivityTest.kt index b7155f74c2..20b8f5659c 100644 --- a/core/activity/src/test/kotlin/app/pachli/core/activity/BottomSheetActivityTest.kt +++ b/core/activity/src/test/kotlin/app/pachli/core/activity/BottomSheetActivityTest.kt @@ -23,7 +23,7 @@ import app.pachli.core.network.model.Status import app.pachli.core.network.model.TimelineAccount import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.testing.rules.MainCoroutineRule -import at.connyduck.calladapter.networkresult.NetworkResult +import app.pachli.core.testing.success import java.time.Instant import java.util.Date import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,7 +55,7 @@ class BottomSheetActivityTest { 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 emptyResponse = NetworkResult.success( + private val emptyResponse = success( SearchResult(emptyList(), emptyList(), emptyList()), ) @@ -71,7 +71,7 @@ class BottomSheetActivityTest { avatar = "", createdAt = Instant.now(), ) - private val accountResponse = NetworkResult.success( + private val accountResponse = success( SearchResult(listOf(account), emptyList(), emptyList()), ) @@ -106,7 +106,7 @@ class BottomSheetActivityTest { language = null, filtered = null, ) - private val statusResponse = NetworkResult.success( + private val statusResponse = success( SearchResult(emptyList(), listOf(status), emptyList()), ) diff --git a/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRemoteMediator.kt b/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRemoteMediator.kt index ceb75d7106..5f5dbf4ac5 100644 --- a/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRemoteMediator.kt +++ b/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRemoteMediator.kt @@ -43,13 +43,16 @@ import app.pachli.core.network.model.Report import app.pachli.core.network.model.Status import app.pachli.core.network.model.TimelineAccount import app.pachli.core.network.retrofit.MastodonApi +import app.pachli.core.network.retrofit.apiresult.ApiResponse +import app.pachli.core.network.retrofit.apiresult.ApiResult +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.get +import com.github.michaelbull.result.getOrElse import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import okhttp3.Headers -import retrofit2.HttpException -import retrofit2.Response import timber.log.Timber @OptIn(ExperimentalPagingApi::class) @@ -94,18 +97,14 @@ class NotificationsRemoteMediator( ) ?: return@transactionProvider MediatorResult.Success(endOfPaginationReached = true) mastodonApi.notifications(maxId = rke.key, limit = state.config.pageSize) } - } - - val notifications = response.body() - if (!response.isSuccessful || notifications == null) { - return@transactionProvider MediatorResult.Error(HttpException(response)) - } + }.getOrElse { return@transactionProvider MediatorResult.Error(it.throwable) } + val notifications = response.body if (notifications.isEmpty()) { return@transactionProvider MediatorResult.Success(endOfPaginationReached = loadType != LoadType.REFRESH) } - val links = Links.from(response.headers()["link"]) + val links = Links.from(response.headers["link"]) when (loadType) { LoadType.REFRESH -> { @@ -169,7 +168,7 @@ class NotificationsRemoteMediator( * @return The initial page of notifications centered on the notification with * [notificationId], or the most recent notifications if [notificationId] is null. */ - private suspend fun getInitialPage(notificationId: String?, pageSize: Int): Response> = + private suspend fun getInitialPage(notificationId: String?, pageSize: Int): ApiResult> = coroutineScope { notificationId ?: return@coroutineScope mastodonApi.notifications(limit = pageSize) @@ -178,9 +177,9 @@ class NotificationsRemoteMediator( val nextPage = async { mastodonApi.notifications(maxId = notificationId, limit = pageSize * 3) } val notifications = buildList { - prevPage.await().body()?.let { this.addAll(it) } - notification.await().getOrNull()?.let { this.add(it) } - nextPage.await().body()?.let { this.addAll(it) } + prevPage.await().get()?.let { this.addAll(it.body) } + notification.await().get()?.let { this.add(it.body) } + nextPage.await().get()?.let { this.addAll(it.body) } } val minId = notifications.firstOrNull()?.id ?: notificationId @@ -190,7 +189,7 @@ class NotificationsRemoteMediator( .add("link: ; rel=\"next\", ; rel=\"prev\"") .build() - return@coroutineScope Response.success(notifications, headers) + return@coroutineScope Ok(ApiResponse(headers, notifications, 200)) } /** diff --git a/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRepository.kt b/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRepository.kt index dd1556aeae..8b0ce7d086 100644 --- a/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRepository.kt +++ b/core/data/src/main/kotlin/app/pachli/core/data/repository/notifications/NotificationsRepository.kt @@ -17,7 +17,6 @@ package app.pachli.core.data.repository.notifications -import androidx.annotation.StringRes import androidx.paging.ExperimentalPagingApi import androidx.paging.InvalidatingPagingSourceFactory import androidx.paging.Pager @@ -47,11 +46,12 @@ import app.pachli.core.model.AccountFilterDecision import app.pachli.core.model.FilterAction import app.pachli.core.network.model.Notification import app.pachli.core.network.retrofit.MastodonApi -import at.connyduck.calladapter.networkresult.onFailure -import at.connyduck.calladapter.networkresult.onSuccess +import app.pachli.core.network.retrofit.apiresult.ApiError import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -69,23 +69,18 @@ import timber.log.Timber * @param throwable The wrapped throwable. */ // TODO: The API calls should return an ApiResult, then this can wrap those. -sealed class StatusActionError(open val throwable: Throwable) : PachliError { - @get:StringRes - override val resourceId = app.pachli.core.network.R.string.error_generic_fmt - override val formatArgs: Array? = arrayOf(throwable) - override val cause: PachliError? = null - +sealed class StatusActionError(open val error: ApiError) : PachliError by error { /** Bookmarking a status failed. */ - data class Bookmark(override val throwable: Throwable) : StatusActionError(throwable) + data class Bookmark(override val error: ApiError) : StatusActionError(error) /** Favouriting a status failed. */ - data class Favourite(override val throwable: Throwable) : StatusActionError(throwable) + data class Favourite(override val error: ApiError) : StatusActionError(error) /** Reblogging a status failed. */ - data class Reblog(override val throwable: Throwable) : StatusActionError(throwable) + data class Reblog(override val error: ApiError) : StatusActionError(error) /** Voting in a poll failed. */ - data class VoteInPoll(override val throwable: Throwable) : StatusActionError(throwable) + data class VoteInPoll(override val error: ApiError) : StatusActionError(error) } /** @@ -166,9 +161,7 @@ class NotificationsRepository @Inject constructor( * if successful. */ suspend fun clearNotifications() = externalScope.async { - return@async mastodonApi.clearNotifications().apply { - if (isSuccessful) this@NotificationsRepository.invalidate() - } + return@async mastodonApi.clearNotifications().onSuccess { invalidate() } }.await() /** @@ -369,16 +362,12 @@ class NotificationsRepository @Inject constructor( pollId: String, choices: List, ): Result = externalScope.async { - if (choices.isEmpty()) { - return@async Err(StatusActionError.VoteInPoll(IllegalStateException())) - } - mastodonApi.voteInPoll(pollId, choices) .onSuccess { poll -> - statusDao.setVoted(pachliAccountId, statusId, poll) - eventHub.dispatch(PollVoteEvent(statusId, poll)) + statusDao.setVoted(pachliAccountId, statusId, poll.body) + eventHub.dispatch(PollVoteEvent(statusId, poll.body)) } - .onFailure { throwable -> return@async Err(StatusActionError.VoteInPoll(throwable)) } + .onFailure { return@async Err(StatusActionError.VoteInPoll(it)) } return@async Ok(Unit) }.await() diff --git a/core/network-test/build.gradle.kts b/core/network-test/build.gradle.kts index 76ccef89bb..7c5298272f 100644 --- a/core/network-test/build.gradle.kts +++ b/core/network-test/build.gradle.kts @@ -39,4 +39,7 @@ dependencies { implementation(libs.moshi) implementation(libs.moshi.adapters) ksp(libs.moshi.codegen) + + implementation(libs.bundles.okhttp) + ?.because("FakeNetworkModule mocks OkHttpClient") } diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 3b7cf5cfa3..446ce4e6d1 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -40,7 +40,6 @@ dependencies { implementation(libs.bundles.retrofit) implementation(libs.bundles.okhttp) - api(libs.networkresult.calladapter) implementation(libs.semver) testImplementation(libs.mockwebserver) diff --git a/core/network/src/main/kotlin/app/pachli/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/app/pachli/core/network/di/NetworkModule.kt index 8606ba89ae..6c94671aca 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/di/NetworkModule.kt @@ -41,7 +41,6 @@ import app.pachli.core.preferences.PrefKeys.HTTP_PROXY_SERVER import app.pachli.core.preferences.ProxyConfiguration import app.pachli.core.preferences.SharedPreferencesRepository import app.pachli.core.preferences.getNonNullString -import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides @@ -147,7 +146,6 @@ object NetworkModule { .addConverterFactory(NewContentFilterConverterFactory) .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResultCallAdapterFactory.create()) - .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) .build() } diff --git a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt index b23552235d..675851c429 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt @@ -54,12 +54,8 @@ import app.pachli.core.network.model.TrendingTag import app.pachli.core.network.model.TrendsLink import app.pachli.core.network.model.UserListRepliesPolicy import app.pachli.core.network.retrofit.apiresult.ApiResult -import at.connyduck.calladapter.networkresult.NetworkResult import okhttp3.MultipartBody import okhttp3.RequestBody -import okhttp3.ResponseBody -import retrofit2.Call -import retrofit2.Response import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.Field @@ -115,13 +111,12 @@ interface MastodonApi { ): ApiResult @GET("api/v1/timelines/home") - @Throws(Exception::class) suspend fun homeTimeline( @Query("max_id") maxId: String? = null, @Query("min_id") minId: String? = null, @Query("since_id") sinceId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @GET("api/v1/timelines/public") suspend fun publicTimeline( @@ -130,7 +125,7 @@ interface MastodonApi { @Query("since_id") sinceId: String? = null, @Query("min_id") minId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @GET("api/v1/timelines/tag/{hashtag}") suspend fun hashtagTimeline( @@ -141,7 +136,7 @@ interface MastodonApi { @Query("since_id") sinceId: String? = null, @Query("min_id") minId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @GET("api/v1/timelines/list/{listId}") suspend fun listTimeline( @@ -150,7 +145,7 @@ interface MastodonApi { @Query("since_id") sinceId: String? = null, @Query("min_id") minId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @GET("api/v1/timelines/link") suspend fun linkTimeline( @@ -158,7 +153,7 @@ interface MastodonApi { @Query("max_id") maxId: String? = null, @Query("min_id") minId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @GET("api/v1/notifications") suspend fun notifications( @@ -172,20 +167,20 @@ interface MastodonApi { @Query("exclude_types[]") excludes: Set? = null, /** Include notifications filtered by the user's notifications filter policy. */ @Query("include_filtered") includeFiltered: Boolean = true, - ): Response> + ): ApiResult> /** Fetch a single notification */ @GET("api/v1/notifications/{id}") suspend fun notification( @Path("id") id: String, - ): NetworkResult + ): ApiResult @GET("api/v1/markers") suspend fun markersWithAuth( @Header("Authorization") auth: String, @Header(DOMAIN_HEADER) domain: String, @Query("timeline[]") timelines: List, - ): Map + ): ApiResult> @FormUrlEncoded @POST("api/v1/markers") @@ -194,7 +189,7 @@ interface MastodonApi { @Header(DOMAIN_HEADER) domain: String, @Field("home[last_read_id]") homeLastReadId: String? = null, @Field("notifications[last_read_id]") notificationsLastReadId: String? = null, - ): NetworkResult + ): ApiResult @GET("api/v1/notifications") suspend fun notificationsWithAuth( @@ -205,7 +200,7 @@ interface MastodonApi { ): ApiResult> @POST("api/v1/notifications/clear") - suspend fun clearNotifications(): Response + suspend fun clearNotifications(): ApiResult @FormUrlEncoded @PUT("api/v1/media/{mediaId}") @@ -226,7 +221,7 @@ interface MastodonApi { @Header(DOMAIN_HEADER) domain: String, @Header("Idempotency-Key") idempotencyKey: String, @Body status: NewStatus, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses") suspend fun createScheduledStatus( @@ -234,12 +229,12 @@ interface MastodonApi { @Header(DOMAIN_HEADER) domain: String, @Header("Idempotency-Key") idempotencyKey: String, @Body status: NewStatus, - ): NetworkResult + ): ApiResult @GET("api/v1/statuses/{id}") suspend fun status( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @PUT("api/v1/statuses/{id}") suspend fun editStatus( @@ -248,22 +243,22 @@ interface MastodonApi { @Header(DOMAIN_HEADER) domain: String, @Header("Idempotency-Key") idempotencyKey: String, @Body editedStatus: NewStatus, - ): NetworkResult + ): ApiResult @GET("api/v1/statuses/{id}/source") suspend fun statusSource( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @GET("api/v1/statuses/{id}/context") suspend fun statusContext( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @GET("api/v1/statuses/{id}/history") suspend fun statusEdits( @Path("id") statusId: String, - ): NetworkResult> + ): ApiResult> @GET("api/v1/statuses/{id}/reblogged_by") suspend fun statusRebloggedBy( @@ -280,73 +275,73 @@ interface MastodonApi { @DELETE("api/v1/statuses/{id}") suspend fun deleteStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult - @POST("api/v1/statuses/{id}/reblog") + @POST("api/v1/statuses/{id}/reblog2") suspend fun reblogStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult - @POST("api/v1/statuses/{id}/unreblog") + @POST("api/v1/statuses/{id}/unreblog2") suspend fun unreblogStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/favourite") suspend fun favouriteStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/unfavourite") suspend fun unfavouriteStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/bookmark") suspend fun bookmarkStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/unbookmark") suspend fun unbookmarkStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/pin") suspend fun pinStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/unpin") suspend fun unpinStatus( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/mute") suspend fun muteConversation( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/unmute") suspend fun unmuteConversation( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/statuses/{id}/translate") suspend fun translate( @Path("id") statusId: String, - ): NetworkResult + ): ApiResult @GET("api/v1/scheduled_statuses") suspend fun scheduledStatuses( @Query("limit") limit: Int? = null, @Query("max_id") maxId: String? = null, - ): NetworkResult> + ): ApiResult> @DELETE("api/v1/scheduled_statuses/{id}") suspend fun deleteScheduledStatus( @Path("id") scheduledStatusId: String, - ): NetworkResult + ): ApiResult @GET("api/v1/accounts/verify_credentials") suspend fun accountVerifyCredentials( @@ -356,11 +351,11 @@ interface MastodonApi { @FormUrlEncoded @PATCH("api/v1/accounts/update_credentials") - fun accountUpdateSource( + suspend fun accountUpdateSource( @Field("source[privacy]") privacy: String?, @Field("source[sensitive]") sensitive: Boolean?, @Field("source[language]") language: String?, - ): Call + ): ApiResult @Multipart @PATCH("api/v1/accounts/update_credentials") @@ -378,7 +373,7 @@ interface MastodonApi { @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?, - ): NetworkResult + ): ApiResult @GET("api/v1/accounts/search") suspend fun searchAccounts( @@ -415,7 +410,7 @@ interface MastodonApi { @Query("exclude_replies") excludeReplies: Boolean? = null, @Query("only_media") onlyMedia: Boolean? = null, @Query("pinned") pinned: Boolean? = null, - ): Response> + ): ApiResult> @GET("api/v1/accounts/{id}/followers") suspend fun accountFollowers( @@ -496,7 +491,7 @@ interface MastodonApi { @Query("max_id") maxId: String? = null, @Query("since_id") sinceId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @FormUrlEncoded @POST("api/v1/domain_blocks") @@ -515,7 +510,7 @@ interface MastodonApi { @Query("since_id") sinceId: String? = null, @Query("min_id") minId: String? = null, @Query("limit") limit: Int?, - ): Response> + ): ApiResult> @GET("api/v1/bookmarks") suspend fun bookmarks( @@ -523,7 +518,7 @@ interface MastodonApi { @Query("since_id") sinceId: String? = null, @Query("min_id") minId: String? = null, @Query("limit") limit: Int?, - ): Response> + ): ApiResult> @GET("api/v1/follow_requests") suspend fun followRequests( @@ -533,12 +528,12 @@ interface MastodonApi { @POST("api/v1/follow_requests/{id}/authorize") suspend fun authorizeFollowRequest( @Path("id") accountId: String, - ): NetworkResult + ): ApiResult @POST("api/v1/follow_requests/{id}/reject") suspend fun rejectFollowRequest( @Path("id") accountId: String, - ): NetworkResult + ): ApiResult @FormUrlEncoded @POST("api/v1/apps") @@ -548,7 +543,7 @@ interface MastodonApi { @Field("redirect_uris") redirectUris: String, @Field("scopes") scopes: String, @Field("website") website: String, - ): NetworkResult + ): ApiResult @FormUrlEncoded @POST("oauth/token") @@ -559,7 +554,7 @@ interface MastodonApi { @Field("redirect_uri") redirectUri: String, @Field("code") code: String, @Field("grant_type") grantType: String, - ): NetworkResult + ): ApiResult @FormUrlEncoded @POST("oauth/revoke") @@ -624,7 +619,7 @@ interface MastodonApi { suspend fun getConversations( @Query("max_id") maxId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @DELETE("/api/v1/conversations/{id}") suspend fun deleteConversation( @@ -707,7 +702,7 @@ interface MastodonApi { suspend fun voteInPoll( @Path("id") id: String, @Field("choices[]") choices: List, - ): NetworkResult + ): ApiResult @GET("api/v1/announcements") suspend fun listAnnouncements( @@ -717,19 +712,19 @@ interface MastodonApi { @POST("api/v1/announcements/{id}/dismiss") suspend fun dismissAnnouncement( @Path("id") announcementId: String, - ): ApiResult + ): ApiResult @PUT("api/v1/announcements/{id}/reactions/{name}") suspend fun addAnnouncementReaction( @Path("id") announcementId: String, @Path("name") name: String, - ): NetworkResult + ): ApiResult @DELETE("api/v1/announcements/{id}/reactions/{name}") suspend fun removeAnnouncementReaction( @Path("id") announcementId: String, @Path("name") name: String, - ): NetworkResult + ): ApiResult @FormUrlEncoded @POST("api/v1/reports") @@ -738,7 +733,7 @@ interface MastodonApi { @Field("status_ids[]") statusIds: List, @Field("comment") comment: String, @Field("forward") isNotifyRemote: Boolean?, - ): NetworkResult + ): ApiResult @GET("api/v2/search") suspend fun search( @@ -748,7 +743,7 @@ interface MastodonApi { @Query("limit") limit: Int? = null, @Query("offset") offset: Int? = null, @Query("following") following: Boolean? = null, - ): NetworkResult + ): ApiResult @FormUrlEncoded @POST("api/v1/accounts/{id}/note") @@ -778,7 +773,7 @@ interface MastodonApi { ): ApiResult @GET("api/v1/tags/{name}") - suspend fun tag(@Path("name") name: String): NetworkResult + suspend fun tag(@Path("name") name: String): ApiResult @GET("api/v1/followed_tags") suspend fun followedTags( @@ -786,28 +781,28 @@ interface MastodonApi { @Query("since_id") sinceId: String? = null, @Query("max_id") maxId: String? = null, @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @POST("api/v1/tags/{name}/follow") - suspend fun followTag(@Path("name") name: String): NetworkResult + suspend fun followTag(@Path("name") name: String): ApiResult @POST("api/v1/tags/{name}/unfollow") - suspend fun unfollowTag(@Path("name") name: String): NetworkResult + suspend fun unfollowTag(@Path("name") name: String): ApiResult @GET("api/v1/trends/tags") suspend fun trendingTags( @Query("limit") limit: Int? = null, - ): NetworkResult> + ): ApiResult> @GET("api/v1/trends/links") suspend fun trendingLinks( @Query("limit") limit: Int? = null, - ): NetworkResult> + ): ApiResult> @GET("api/v1/trends/statuses") suspend fun trendingStatuses( @Query("limit") limit: Int? = null, - ): Response> + ): ApiResult> @GET("api/v2/suggestions") suspend fun getSuggestions( diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts index 4748f24404..de2fcf8d4a 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -38,6 +38,10 @@ dependencies { implementation(libs.moshi) implementation(libs.moshi.adapters) + implementation(libs.okhttp.core) + ?.because("Includes testing utilities for ApiResult") + implementation(libs.retrofit.core) + ?.because("Includes testing utilities for ApiResult") api(libs.kotlinx.coroutines.test) api(libs.androidx.test.junit) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 1199500a37..a37b832477 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -37,8 +37,10 @@ dependencies { implementation(projects.core.preferences) ?.because("PreferenceEnum types in EnumListPreference") - // Uses HttpException from Retrofit + implementation(libs.retrofit.core) + ?.because("Uses HttpException") implementation(projects.core.network) + ?.because("ThrowableExtensions uses getServerErrorMessage") // Uses JsonDataException from Moshi implementation(libs.moshi) diff --git a/feature/login/src/main/kotlin/app/pachli/feature/login/LoginActivity.kt b/feature/login/src/main/kotlin/app/pachli/feature/login/LoginActivity.kt index 88d5d86c25..cf45301298 100644 --- a/feature/login/src/main/kotlin/app/pachli/feature/login/LoginActivity.kt +++ b/feature/login/src/main/kotlin/app/pachli/feature/login/LoginActivity.kt @@ -41,9 +41,7 @@ import app.pachli.core.navigation.LoginActivityIntent import app.pachli.core.navigation.MainActivityIntent import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.preferences.getNonNullString -import app.pachli.core.ui.extensions.getErrorString import app.pachli.feature.login.databinding.ActivityLoginBinding -import at.connyduck.calladapter.networkresult.fold import com.bumptech.glide.Glide import com.github.michaelbull.result.Result import com.github.michaelbull.result.onFailure @@ -221,33 +219,31 @@ class LoginActivity : BaseActivity() { oauthRedirectUri, OAUTH_SCOPES, getString(R.string.pachli_website), - ).fold( - { credentials -> - // Before we open browser page we save the data. - // 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. - // Alternatively we could pass it all as part of the intent and receive it back - // but it is a bit of a workaround. - preferences.edit() - .putString(DOMAIN, domain) - .putString(CLIENT_ID, credentials.clientId) - .putString(CLIENT_SECRET, credentials.clientSecret) - .apply() - - redirectUserToAuthorizeAndLogin(domain, credentials.clientId, openInWebView) - }, - { e -> - binding.loginButton.isEnabled = true - binding.domainTextInputLayout.error = - String.format( - getString(R.string.error_failed_app_registration_fmt), - e.getErrorString(this@LoginActivity), - ) - setLoading(false) - Timber.e(e, "Error when creating/registing app") - return@launch - }, - ) + ).onSuccess { + val credentials = it.body + // Before we open browser page we save the data. + // 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. + // Alternatively we could pass it all as part of the intent and receive it back + // but it is a bit of a workaround. + preferences.edit() + .putString(DOMAIN, domain) + .putString(CLIENT_ID, credentials.clientId) + .putString(CLIENT_SECRET, credentials.clientSecret) + .apply() + + redirectUserToAuthorizeAndLogin(domain, credentials.clientId, openInWebView) + }.onFailure { e -> + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = + getString( + R.string.error_failed_app_registration_fmt, + e.fmt(this@LoginActivity), + ) + setLoading(false) + Timber.e("Error when creating/registing app: %s", e.fmt(this@LoginActivity)) + return@launch + } } } @@ -333,25 +329,22 @@ class LoginActivity : BaseActivity() { oauthRedirectUri, code, "authorization_code", - ).fold( - { accessToken -> - viewModel.accept( - FallibleUiAction.VerifyAndAddAccount( - accessToken, - domain, - clientId, - clientSecret, - OAUTH_SCOPES, - ), - ) - }, - { e -> - setLoading(false) - binding.domainTextInputLayout.error = - getString(R.string.error_retrieving_oauth_token) - Timber.e(e, getString(R.string.error_retrieving_oauth_token)) - }, - ) + ).onSuccess { + val accessToken = it.body + viewModel.accept( + FallibleUiAction.VerifyAndAddAccount( + accessToken, + domain, + clientId, + clientSecret, + OAUTH_SCOPES, + ), + ) + }.onFailure { e -> + setLoading(false) + binding.domainTextInputLayout.error = + getString(R.string.error_retrieving_oauth_token_fmt, e.fmt(this@LoginActivity)) + } } private fun setLoading(loadingState: Boolean) { diff --git a/feature/login/src/main/res/values-ar/strings.xml b/feature/login/src/main/res/values-ar/strings.xml index 22cc603b92..7ca4574cf5 100644 --- a/feature/login/src/main/res/values-ar/strings.xml +++ b/feature/login/src/main/res/values-ar/strings.xml @@ -13,7 +13,7 @@ %1$s :أخففت المصادقة مع مثيل الخادم هذا لقد وقع هناك خطأ مجهول في التصريح. تم رفض التصريح. - فشل الحصول على رمز الولوج. + %1$s: فشل الحصول على رمز الولوج. %1$s: فشلت عملية تحميل تفاصيل الحساب أي مثيل خادم؟ الولوج باستخدام Pachli diff --git a/feature/login/src/main/res/values-be/strings.xml b/feature/login/src/main/res/values-be/strings.xml index 3f3176b48c..dcfb54af5f 100644 --- a/feature/login/src/main/res/values-be/strings.xml +++ b/feature/login/src/main/res/values-be/strings.xml @@ -13,7 +13,7 @@ Памылка праверкі сапраўднасці на гэтым серверы. Калі праблема застаецца, паспрабуйце ўвайсці з браўзера з меню: %1$s Адбылася невядомая памылка праверкі сапраўднасці. Калі праблема застаецца, паспрабуйце ўвайсці з браўзера з меню. Аўтарызацыя была адхілена. Калі ўпэўнены што ўвялі сапраўдныя ўліковыя даныя, паспрабуйце ўвайсці з браўзера з меню. - Токен уваходу не атрыманы. Калі праблема застаецца, паспрабуйце ўвайсці з браўзера з меню. + Токен уваходу не атрыманы. Калі праблема застаецца, паспрабуйце ўвайсці з браўзера з меню: %1$s Дэталі ўліковага запісу не атрыманы: %1$s Які сервер\? Увайсці з Pachli diff --git a/feature/login/src/main/res/values-bg/strings.xml b/feature/login/src/main/res/values-bg/strings.xml index 0e499f33b2..4a410f06af 100644 --- a/feature/login/src/main/res/values-bg/strings.xml +++ b/feature/login/src/main/res/values-bg/strings.xml @@ -16,7 +16,7 @@ Неуспешно удостоверяване с тази инстанция: %1$s Възникна неидентифицирана грешка при упълномощаване. Упълномощаването е отказано. - Получаването на токен за вход бе неуспешно. + Получаването на токен за вход бе неуспешно: %1$s Коя инстанция\? Влизане с Mastodon Какво е инстанция\? diff --git a/feature/login/src/main/res/values-bn-rBD/strings.xml b/feature/login/src/main/res/values-bn-rBD/strings.xml index a4e5ba13a1..bb833b3dbc 100644 --- a/feature/login/src/main/res/values-bn-rBD/strings.xml +++ b/feature/login/src/main/res/values-bn-rBD/strings.xml @@ -20,7 +20,7 @@ এই ইনস্ট্যান্স এর সঙ্গে প্রমাণীকরণ ব্যর্থ।: %1$s একটি অজ্ঞাত প্রমাণীকরণ ত্রুটি ঘটেছে। অনুমোদন অস্বীকার করা হয়েছে। - একটি লগইন টোকেন পেতে ব্যর্থ। + একটি লগইন টোকেন পেতে ব্যর্থ।: %1$s কোন ইনস্ট্যান্স\? মাস্টোডনের সঙ্গে লগইন করো ইনস্ট্যান্স কি\? diff --git a/feature/login/src/main/res/values-bn-rIN/strings.xml b/feature/login/src/main/res/values-bn-rIN/strings.xml index 2b982d7883..1e7c8e7647 100644 --- a/feature/login/src/main/res/values-bn-rIN/strings.xml +++ b/feature/login/src/main/res/values-bn-rIN/strings.xml @@ -1,20 +1,20 @@ - কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন mastodon.social, icosahedron.website, social.tchncs.de, এবং আরও! -\n -\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। -\n -\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। -\n + কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন mastodon.social, icosahedron.website, social.tchncs.de, এবং আরও! +\n +\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। +\n +\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। +\n \nআরো তথ্য joinmastodon.org এ পাওয়া যেতে পারে। বদ্ধ অবৈধ ডোমেইন প্রবেশ করানো হয়েছে এই ইনস্ট্যান্স এর সঙ্গে প্রমাণীকরণ ব্যর্থ।: %1$s একটি অজ্ঞাত প্রমাণীকরণ ত্রুটি ঘটেছে। অনুমোদন অস্বীকার করা হয়েছে। - একটি লগইন টোকেন পেতে ব্যর্থ। + একটি লগইন টোকেন পেতে ব্যর্থ।: %1$s কোন ইনস্ট্যান্স\? মাস্টোডনের সঙ্গে লগইন করো ইনস্ট্যান্স কি\? সংযুক্ত হচ্ছে … - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-ca/strings.xml b/feature/login/src/main/res/values-ca/strings.xml index 12a1dc6f94..f1f81d610b 100644 --- a/feature/login/src/main/res/values-ca/strings.xml +++ b/feature/login/src/main/res/values-ca/strings.xml @@ -13,7 +13,7 @@ No s\'ha pogut autenticar amb aquesta instància. Si això continua, proveu d\'iniciar sessió al navegador des del menú: %1$s S\'ha produït un error d\'autorització no identificat. Si això continua, proveu d\'iniciar sessió al navegador des del menú. S\'ha denegat l\'autorització. Si esteu segur que heu proporcionat les credencials correctes, proveu d\'iniciar sessió al navegador des del menú. - No s\'ha pogut obtenir el token d\'inici de sessió. Si això continua, proveu d\'iniciar sessió al navegador des del menú. + No s\'ha pogut obtenir el token d\'inici de sessió. Si això continua, proveu d\'iniciar sessió al navegador des del menú: %1$s No s\'han pogut carregar els detalls del compte: %1$s Quina instància? Inicia sessió amb Pachli diff --git a/feature/login/src/main/res/values-ckb/strings.xml b/feature/login/src/main/res/values-ckb/strings.xml index 1becb5613a..4677b5347b 100644 --- a/feature/login/src/main/res/values-ckb/strings.xml +++ b/feature/login/src/main/res/values-ckb/strings.xml @@ -1,20 +1,20 @@ - ناونیشان یان دۆمەینی هەر نمونەیەک دەکرێت لێرە تێبنووسرێت، وەک فرەتر! -\n -\nئەگەر هێشتا ئەژمێرێکت نیە، دەتوانیت ناوی ئەو نمونەیە داخڵ بکەیت کە دەتەوێت بیبەستیت و ئەژمێرێک دروست بکەیت لەوێ. -\n -\nنموونەیەک تاکە شوێنە کە ئەژمێرەکەت میوانداری کراوە، بەڵام دەتوانیت بە ئاسانی پەیوەندی لەگەڵ بکەیت و دوای ئەو خەڵکانە بکەویت لە نمونەکانی تر وەک ئەوەی تۆ لە هەمان سایت دابیت. -\n + ناونیشان یان دۆمەینی هەر نمونەیەک دەکرێت لێرە تێبنووسرێت، وەک فرەتر! +\n +\nئەگەر هێشتا ئەژمێرێکت نیە، دەتوانیت ناوی ئەو نمونەیە داخڵ بکەیت کە دەتەوێت بیبەستیت و ئەژمێرێک دروست بکەیت لەوێ. +\n +\nنموونەیەک تاکە شوێنە کە ئەژمێرەکەت میوانداری کراوە، بەڵام دەتوانیت بە ئاسانی پەیوەندی لەگەڵ بکەیت و دوای ئەو خەڵکانە بکەویت لە نمونەکانی تر وەک ئەوەی تۆ لە هەمان سایت دابیت. +\n \nزانیاری زیاتر دەتوانرێت بدۆزرێتەوە لە joinmastodon.org. دابخە دۆمەینێکی نادروستت نووسیوە %1$s :سەرکەوتوو نەبوو، ڕاستکردنەوە لەگەڵ ئەم نمونەیە هەڵەیەک بۆ مۆڵەتدانی نەناسراو ڕووی دا. ڕێپێدان ڕەتکرایەوە. - سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە. + %1$s :سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە. کام نموونە؟ چوونەژوورەوە لەگەڵ ماستۆدۆن نموونەیەک چییە؟ گرێدان… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-cs/strings.xml b/feature/login/src/main/res/values-cs/strings.xml index 9f2a36616b..bd8133775e 100644 --- a/feature/login/src/main/res/values-cs/strings.xml +++ b/feature/login/src/main/res/values-cs/strings.xml @@ -12,7 +12,7 @@ Autentizace s tímto serverem nebyla úspěšná: %1$s Vyskytla se neidentifikovaná chyba autorizace. Autorizace byla zamítnuta. - Nepodařilo se získat přihlašovací token. + Nepodařilo se získat přihlašovací token: %1$s Nepodařilo se načíst detaily účtu: %1$s Který server? Přihlásit se účtem Mastodon diff --git a/feature/login/src/main/res/values-cy/strings.xml b/feature/login/src/main/res/values-cy/strings.xml index 5e9500ab7a..b140e157e2 100644 --- a/feature/login/src/main/res/values-cy/strings.xml +++ b/feature/login/src/main/res/values-cy/strings.xml @@ -13,7 +13,7 @@ Methu awdurdodi gyda\'r gweinydd hwnnw. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen: %1$s Bu gwall awdurdodi anhysbys. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen. Gwrthodwyd awdurdodi. Os ydych chi\'n siŵr dy fod di wedi gyflenwi\'r manylion cywir, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen. - Methu cael tocyn mewngofnodi. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen. + Methu cael tocyn mewngofnodi. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen: %1$s Wedi methu llwytho manylion cyfrif: %1$s Pa weinydd\? Mewngofnodi â Pachli diff --git a/feature/login/src/main/res/values-de/strings.xml b/feature/login/src/main/res/values-de/strings.xml index 5088df1d83..c5a7233111 100644 --- a/feature/login/src/main/res/values-de/strings.xml +++ b/feature/login/src/main/res/values-de/strings.xml @@ -13,7 +13,7 @@ Authentifizieren mit dieser Instanz fehlgeschlagen. Falls das Problem weiter besteht, versuche dich über den Browser anzumelden: %1$s Ein unbekannter Fehler ist bei der Autorisierung aufgetreten. Falls das Problem weiter besteht, versuche dich über den Browser anzumelden. Autorisierung wurde abgelehnt. Wenn du dir sicher bist, dass du die korrekten Anmeldedaten eingegeben hast, versuche dich über den Browser anzumelden. - Es konnte kein Anmelde-Token abgerufen werden. Falls das Problem weiter besteht, versuche dich über den Browser anzumelden. + Es konnte kein Anmelde-Token abgerufen werden. Falls das Problem weiter besteht, versuche dich über den Browser anzumelden: %1$s Fehler beim Laden der Kontodetails: %1$s Welche Instanz? Mit Pachli anmelden diff --git a/feature/login/src/main/res/values-en-rGB/strings.xml b/feature/login/src/main/res/values-en-rGB/strings.xml index a162e6059e..65d987ce80 100644 --- a/feature/login/src/main/res/values-en-rGB/strings.xml +++ b/feature/login/src/main/res/values-en-rGB/strings.xml @@ -4,7 +4,7 @@ Failed authenticating with that instance: %1$s An unidentified authorisation error occurred. Authorisation was denied. - Failed getting a login token. If this persists, try Login in Browser from the menu. + Failed getting a login token. If this persists, try Login in Browser from the menu: %1$s Failed loading account details: %1$s Login Could not load the login page. diff --git a/feature/login/src/main/res/values-eo/strings.xml b/feature/login/src/main/res/values-eo/strings.xml index 4cb3eabc81..71896ab6cf 100644 --- a/feature/login/src/main/res/values-eo/strings.xml +++ b/feature/login/src/main/res/values-eo/strings.xml @@ -12,7 +12,7 @@ Aŭtentigo en ĉi tiu nodo malsukcesis: %1$s Nekonata eraro de aŭtentigo okazis. Rajtigo rifuzita. - Akiro de atingoĵetono malsukcesis. + Akiro de atingoĵetono malsukcesis: %1$s Ŝargo de detaloj pri la konto malsukcesis: %1$s Kiu nodo? Ensaluti al Mastodon diff --git a/feature/login/src/main/res/values-es/strings.xml b/feature/login/src/main/res/values-es/strings.xml index 330d50bd7f..e26b90a14a 100644 --- a/feature/login/src/main/res/values-es/strings.xml +++ b/feature/login/src/main/res/values-es/strings.xml @@ -7,7 +7,7 @@ Fallo de autenticación con esta instancia. Si esto persiste, prueba en el menú Iniciar sesión con el navegador: %1$s Ocurrió un error de autorización no identificado. Si esto persiste, prueba en el menú Iniciar sesión con el navegador. La autorización falló. Si estás seguro de que has suministrado las credenciales correctas, prueba en el menú Iniciar sesión con el navegador. - Fallo al obtener identificador de login. Si esto persiste, prueba en el menú Iniciar sesión con el navegador. + Fallo al obtener identificador de login. Si esto persiste, prueba en el menú Iniciar sesión con el navegador: %1$s Falló cargar los detalles de la cuenta: %1$s ¿Qué instancia\? Iniciar sesión con Pachli diff --git a/feature/login/src/main/res/values-eu/strings.xml b/feature/login/src/main/res/values-eu/strings.xml index 8a92daa06a..9af175c41b 100644 --- a/feature/login/src/main/res/values-eu/strings.xml +++ b/feature/login/src/main/res/values-eu/strings.xml @@ -13,7 +13,7 @@ Akatsa instantzia horrekin autentikatzerakoan. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu: %1$s Identifikatu gabeko baimen-akatsa gertatu da. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu. Baimena ukatu da. Ziur bazaude zuk sartutako egiaztagiriak zuzenak direla, menutik, nabigatzailean saioa hasteko aukerarekin saiatu. - Akatsa saio-hasieraren identifikatzailea eskuratzerakoan. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu. + Akatsa saio-hasieraren identifikatzailea eskuratzerakoan. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu: %1$s Akatsa kontuaren xehetasunak kargatzerakoan: %1$s Zein instantzia\? Pachlikin saioa hasi diff --git a/feature/login/src/main/res/values-fa/strings.xml b/feature/login/src/main/res/values-fa/strings.xml index f2792db9ac..2fba6e4d3e 100644 --- a/feature/login/src/main/res/values-fa/strings.xml +++ b/feature/login/src/main/res/values-fa/strings.xml @@ -13,7 +13,7 @@ %1$s: احراز هویت با این نمونه شکست خورد. اگر مشکل ادامه داشت، ورود در مرورگر را از فهرست بیازمایید. خطای احراز هویت ناشناخته‌ای رخ داد. اگر مشکل ادامه داشت، ورود در مرورگر را از فهرست بیازمایید. احراز هویت رد شد. اگر مطمئنید که اطّلاعات را درست وارد کرده‌اید، ورود در مرورگر را از فهرست بیازمایید. - دریافت ژتون ورود شکست خورد. اگر مشکل ادامه داشت، ورود در مرورگر را از فهرست بیازمایید. + %1$s: دریافت ژتون ورود شکست خورد. اگر مشکل ادامه داشت، ورود در مرورگر را از فهرست بیازمایید. %1$s: شکست در بار کردن جزییات حساب کدام نمونه؟ ورود با تاسکی diff --git a/feature/login/src/main/res/values-fi/strings.xml b/feature/login/src/main/res/values-fi/strings.xml index 5cd3feab4a..7390fb53fd 100644 --- a/feature/login/src/main/res/values-fi/strings.xml +++ b/feature/login/src/main/res/values-fi/strings.xml @@ -20,7 +20,7 @@ \nInstanssi on paikka, jossa tilisi sijaitsee, mutta voit helposti olla yhteydessä ja seurata ihmisiä toisilla instansseilla, aivan kuin olisitte samalla sivustolla. \n \nLisää tietoa täällä: joinmastodon.org \u0020 - Kirjautumispolettia ei saatu. Jos tämä jatkuu, valitse valikosta Kirjaudu selaimella. + Kirjautumispolettia ei saatu. Jos tämä jatkuu, valitse valikosta Kirjaudu selaimella: %1$s Palvelimen %s säännöt Kirjautumalla sisään hyväksyt palvelimen %s säännöt. diff --git a/feature/login/src/main/res/values-fr/strings.xml b/feature/login/src/main/res/values-fr/strings.xml index 34f955b15c..c9b5cdad55 100644 --- a/feature/login/src/main/res/values-fr/strings.xml +++ b/feature/login/src/main/res/values-fr/strings.xml @@ -13,7 +13,7 @@ Échec d’authentification auprès de l’instance. Si cela continue à se produire, essayez Se connecter avec le navigateur depuis le menu: %1$s Une erreur d’autorisation inconnue s’est produite. Si cela continue à se produire, essayez Se connecter avec le navigateur depuis le menu. Authentification refusée. Si vous êtes sur·e d\'avoir entré les bons identifiants, essayez Se connecter avec le navigateur depuis le menu. - Impossible de récupérer le jeton d’authentification. Si cela continue à se produire, essayez Se connecter avec le navigateur depuis le menu. + Impossible de récupérer le jeton d’authentification. Si cela continue à se produire, essayez Se connecter avec le navigateur depuis le menu: %1$s Les détails du compte n\'ont pas pu être chargés: %1$s Quelle instance ? Se connecter avec Pachli diff --git a/feature/login/src/main/res/values-fy/strings.xml b/feature/login/src/main/res/values-fy/strings.xml index 1408ca1ae8..9da609656c 100644 --- a/feature/login/src/main/res/values-fy/strings.xml +++ b/feature/login/src/main/res/values-fy/strings.xml @@ -4,7 +4,7 @@ Ûnjildich domein ynfierd Der die harren in net definiearre flater foar. Ferifikaasje ôfkard. - Koe gjin ynlogtoken krije. + Koe gjin ynlogtoken krije: %1$s Ynlogge mei Mastodon Oan it ferbinen… diff --git a/feature/login/src/main/res/values-ga/strings.xml b/feature/login/src/main/res/values-ga/strings.xml index c4f3fae821..3517c43700 100644 --- a/feature/login/src/main/res/values-ga/strings.xml +++ b/feature/login/src/main/res/values-ga/strings.xml @@ -6,7 +6,7 @@ Theip ar fhíordheimhniú leis an ásc sin: %1$s Tharla earráid údaraithe neamhaitheanta. Diúltaíodh údarú. - Theip ar chomhartha logála isteach a fháil. + Theip ar chomhartha logála isteach a fháil: %1$s Theip ar sonraí an chuntais a lódáil: %1$s Cén ásc\? Logáil isteach le Mastodon diff --git a/feature/login/src/main/res/values-gd/strings.xml b/feature/login/src/main/res/values-gd/strings.xml index d334cc0dfa..673a5f380d 100644 --- a/feature/login/src/main/res/values-gd/strings.xml +++ b/feature/login/src/main/res/values-gd/strings.xml @@ -11,7 +11,7 @@ Dh’fhàillig leis an dearbhadh leis an ionstans ud. Ma mhaireas an duilgheadas seo, feuch “Clàraich a-steach le brabhsair” on chlàr-taice: %1$s Thachair mearachd leis an ùghdarrachadh nach do dh’aithnich sinn. Ma mhaireas an duilgheadas seo, feuch “Clàraich a-steach le brabhsair” on chlàr-taice. Chaidh an t-ùghdarrachadh a dhiùltadh. Ma tha thu cinnteach gun do chuir thu a-steach an teisteas ceart, feuch “Clàraich a-steach le brabhsair” on chlàr-taice. - Cha deach leinn tòcan clàraidh a-steach fhaighinn. Ma mhaireas an duilgheadas seo, feuch “Clàraich a-steach le brabhsair” on chlàr-taice. + Cha deach leinn tòcan clàraidh a-steach fhaighinn. Ma mhaireas an duilgheadas seo, feuch “Clàraich a-steach le brabhsair” on chlàr-taice: %1$s Mearachd a’ luchdadh fiosrachadh a’ chunntais: %1$s Cò an t-ionstans\? Clàraich a-steach le Pachli diff --git a/feature/login/src/main/res/values-gl/strings.xml b/feature/login/src/main/res/values-gl/strings.xml index cdb79dcb0a..ccd25e029b 100644 --- a/feature/login/src/main/res/values-gl/strings.xml +++ b/feature/login/src/main/res/values-gl/strings.xml @@ -11,7 +11,7 @@ Fallou a autenticación nesta instancia. Se persiste, inténtao desde Acceder no Navegador no menú: %1$s Aconteceu un erro non identificado na autorización. Se persiste, inténtao desde Acceder no Navedor. A autorización foi rexeitada. Se tes a certeza de que as credenciais son correctas, inténtao desde Acceder no Navegador no menú. - Non se obtivo o token de acceso. Se non o consigues, inténtao desde Acceder no Navegador. + Non se obtivo o token de acceso. Se non o consigues, inténtao desde Acceder no Navegador: %1$s Fallou a carga dos detalles da conta: %1$s En que instancia\? Acceder con Pachli diff --git a/feature/login/src/main/res/values-hi/strings.xml b/feature/login/src/main/res/values-hi/strings.xml index 0841b95e94..1d91f69e00 100644 --- a/feature/login/src/main/res/values-hi/strings.xml +++ b/feature/login/src/main/res/values-hi/strings.xml @@ -5,7 +5,7 @@ उस सर्वर से प्रमाणित करने में विफल।: %1$s एक अज्ञात प्राधिकरण त्रुटि हुई। प्राधिकरण करने के से इनकार कर दिया। - लॉगिन टोकन प्राप्त करने में विफल। + लॉगिन टोकन प्राप्त करने में विफल।: %1$s खाता विवरण लोड करने में विफल रहा: %1$s हिंदी कनेक्ट कर रहे… diff --git a/feature/login/src/main/res/values-hu/strings.xml b/feature/login/src/main/res/values-hu/strings.xml index bf2a0ee9e5..a04fe0c904 100644 --- a/feature/login/src/main/res/values-hu/strings.xml +++ b/feature/login/src/main/res/values-hu/strings.xml @@ -13,7 +13,7 @@ Sikertelen hitelesítés ezen a példányon. Ha ez továbbra is fennáll, próbáld a Bejelentkezés Böngészőben opciót a menüben: %1$s Azonosítatlan hitelesítési hiba történt. Ha ez továbbra is fennáll, próbáld a Bejelentkezés Böngészőben opciót a menüben. Engedély megtagadva. Ha biztos vagy benne, hogy a megfelelő hitelesítési adatokat adtad meg, próbáld a Bejelentkezés Böngészőben funkciót a menüben. - Bejelentkezési token megszerzése sikertelen. Ha ez továbbra is fennáll, próbáld a Bejelentkezés Böngészőben opciót a menüben. + Bejelentkezési token megszerzése sikertelen. Ha ez továbbra is fennáll, próbáld a Bejelentkezés Böngészőben opciót a menüben: %1$s Nem sikerült betölteni a fiókadatokat: %1$s Melyik példány\? Bejelentkezés Pachlival diff --git a/feature/login/src/main/res/values-in/strings.xml b/feature/login/src/main/res/values-in/strings.xml index 5991a0fd1d..19d17612f6 100644 --- a/feature/login/src/main/res/values-in/strings.xml +++ b/feature/login/src/main/res/values-in/strings.xml @@ -5,7 +5,7 @@ Gagal mengautentikasi dengan instance tersebut. Jika ini berlanjut, coba Masuk di Browser dari menu: %1$s "Terjadi kesalahan otorisasi yang tidak diketahui. Jika ini berlanjut, coba Masuk di Browser dari menu." Otorisasi ditolak. Jika Anda yakin telah memberikan kredensial yang benar, coba Masuk di Browser dari menu. - Gagal mendapatkan token masuk. Jika ini terus berlanjut, coba Masuk di Browser dari menu. + Gagal mendapatkan token masuk. Jika ini terus berlanjut, coba Masuk di Browser dari menu: %1$s Gagal memuat detail akun: %1$s Instansi yang mana\? Masuk dengan Mastodon diff --git a/feature/login/src/main/res/values-is/strings.xml b/feature/login/src/main/res/values-is/strings.xml index 4d57b3b2b8..13ca3111aa 100644 --- a/feature/login/src/main/res/values-is/strings.xml +++ b/feature/login/src/main/res/values-is/strings.xml @@ -13,7 +13,7 @@ Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni: %1$s Óskilgreind auðkenningarvilla kom upp. Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni. Heimild var hafnað. Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni. - Mistókst að fá innskráningarteikn. Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni. + Mistókst að fá innskráningarteikn. Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni: %1$s Mistókst að hlaða inn nánari upplýsingum notandaaðgangs: %1$s Hvaða tilvik\? Skrá inn með Pachli diff --git a/feature/login/src/main/res/values-it/strings.xml b/feature/login/src/main/res/values-it/strings.xml index 85a4ec6123..75b35939e7 100644 --- a/feature/login/src/main/res/values-it/strings.xml +++ b/feature/login/src/main/res/values-it/strings.xml @@ -13,7 +13,7 @@ Autenticazione con quell\'istanza fallita. Se il problema persiste, prova a collegarti dal browser nel menu: %1$s Si è verificato un errore di autenticazione non identificato. Se il problema persiste, prova a collegarti dal browser nel menu. Autorizzazione negata. Se sei sicuro di aver usato le credenziali corrette, prova a collegarti con il browser nel menu. - Acquisizione token di accesso fallita. Se il problema persiste, prova a collegarti dal browser nel menu. + Acquisizione token di accesso fallita. Se il problema persiste, prova a collegarti dal browser nel menu: %1$s Caricamento dettagli utente fallito: %1$s Quale istanza? Accedi con Pachli diff --git a/feature/login/src/main/res/values-ja/strings.xml b/feature/login/src/main/res/values-ja/strings.xml index de1cb7ee26..8ef9583e32 100644 --- a/feature/login/src/main/res/values-ja/strings.xml +++ b/feature/login/src/main/res/values-ja/strings.xml @@ -13,7 +13,7 @@ そのインスタンスでの認証に失敗しました。失敗が続く場合、メニューからブラウザでのログインを試してください。: %1$s 不明な承認エラーが発生しました。失敗が続く場合、メニューからブラウザでのログインを試してください。 認証が拒否されました。正しい認証情報を入力したことが確かな場合、メニューからブラウザでのログインを試してください。 - ログイントークンの取得に失敗しました。もし失敗が続く場合、メニューからブラウザでのログインを試してください。 + ログイントークンの取得に失敗しました。もし失敗が続く場合、メニューからブラウザでのログインを試してください。: %1$s アカウントの詳細情報の読み込みに失敗しました: %1$s インスタンス Pachli でログイン diff --git a/feature/login/src/main/res/values-ko/strings.xml b/feature/login/src/main/res/values-ko/strings.xml index 656bb06919..04c9beb0d0 100644 --- a/feature/login/src/main/res/values-ko/strings.xml +++ b/feature/login/src/main/res/values-ko/strings.xml @@ -1,20 +1,20 @@ - 인스턴스의 도메인 주소나 IP주소를 입력하실 수 있습니다. mastodon.social, icosahedron.website, social.tchncs.de 등이 있으며, 그 외에도 더 많은 인스턴스가 당신을 기다리고 있습니다! -\n -\n만약 계정이 없으시다면, 인스턴스 주소를 입력하신 후에 계정을 만드실 수 있습니다. -\n -\n여러분이 어느 인스턴스에 가입하시더라도, 다른 인스턴스에 있는 유저들과 문제 없이 소통하실 수 있습니다. -\n + 인스턴스의 도메인 주소나 IP주소를 입력하실 수 있습니다. mastodon.social, icosahedron.website, social.tchncs.de 등이 있으며, 그 외에도 더 많은 인스턴스가 당신을 기다리고 있습니다! +\n +\n만약 계정이 없으시다면, 인스턴스 주소를 입력하신 후에 계정을 만드실 수 있습니다. +\n +\n여러분이 어느 인스턴스에 가입하시더라도, 다른 인스턴스에 있는 유저들과 문제 없이 소통하실 수 있습니다. +\n \n자세한 사항은 joinmastodon.org을 참조하세요. 닫기 도메인이 올바르지 않습니다 해당 인스턴스에 대한 인증에 실패했습니다: %1$s 알 수 없는 인증 오류가 발생했습니다. 인증이 거부되었습니다. - 로그인 토큰을 받아올 수 없습니다. + 로그인 토큰을 받아올 수 없습니다: %1$s 인스턴스 주소 마스토돈에 로그인 인스턴스가 무엇인가요\? 연결 중… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-lv/strings.xml b/feature/login/src/main/res/values-lv/strings.xml index ef9b409d3a..dc1aa87666 100644 --- a/feature/login/src/main/res/values-lv/strings.xml +++ b/feature/login/src/main/res/values-lv/strings.xml @@ -13,7 +13,7 @@ Autentificēšana ar šo instanci neizdevās. Ja tas atkārtojas, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā: %1$s Radās neidentificēta autorizācijas kļūda. Ja tas atkārtojas, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā. Autorizācija tika liegta. Ja ir pārliecība, ka ievadīti pareizi pieslēgšanās dati, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā. - Neizdevās iegūt pieteikšanās pilnvaru. Ja tas atkārtojas, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā. + Neizdevās iegūt pieteikšanās pilnvaru. Ja tas atkārtojas, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā: %1$s Neizdevās ielādēt konta datus: %1$s Kura instance\? Pieslēgties ar Pachli diff --git a/feature/login/src/main/res/values-ml/strings.xml b/feature/login/src/main/res/values-ml/strings.xml index f1aa9f2c73..219e34c2d1 100644 --- a/feature/login/src/main/res/values-ml/strings.xml +++ b/feature/login/src/main/res/values-ml/strings.xml @@ -5,7 +5,7 @@ ആ ഇൻസ്റ്റൻസുമായി ആധികാരികത ഉറപ്പുവരുത്തുന്നതിൽ പരാജയപ്പെട്ടിരിക്കുന്നു: %1$s അജ്ഞാതമായ ഒരു ആധികാരികതാപിഴവ് സംഭവിച്ചിരിക്കുന്നു. ആധികാരികത ഉറപ്പുവരുത്താനായില്ല. - ഒരു പ്രവേശന ടോക്കൺ ലഭ്യമാക്കുന്നതിൽ പരാജയപ്പെട്ടു. + ഒരു പ്രവേശന ടോക്കൺ ലഭ്യമാക്കുന്നതിൽ പരാജയപ്പെട്ടു: %1$s മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\? diff --git a/feature/login/src/main/res/values-nb-rNO/strings.xml b/feature/login/src/main/res/values-nb-rNO/strings.xml index 9294877751..76016d2e14 100644 --- a/feature/login/src/main/res/values-nb-rNO/strings.xml +++ b/feature/login/src/main/res/values-nb-rNO/strings.xml @@ -7,7 +7,7 @@ Kunne ikke autentisere med den instansen: %1$s En ukjent autoriseringsfeil oppsto. Hvis dette fortsetter, prøv å logge inn i nettleseren fra menyen. Autorisasjon ble nektet. Hvis du er sikker på at du ga riktige data, prøv å logge inn i nettleseren fra menyen. - Henting av logintoken mislyktes. Hvis dette fortsetter prøv å logge in med nettleseren fra menyen. + Henting av logintoken mislyktes. Hvis dette fortsetter prøv å logge in med nettleseren fra menyen: %1$s Lasting av kontodetaljer mislyktes: %1$s Hvilken instanse\? Logg inn med Pachli diff --git a/feature/login/src/main/res/values-nl/strings.xml b/feature/login/src/main/res/values-nl/strings.xml index aff10845a3..e037d58439 100644 --- a/feature/login/src/main/res/values-nl/strings.xml +++ b/feature/login/src/main/res/values-nl/strings.xml @@ -13,7 +13,7 @@ Authenticatie met die server is mislukt. Als het probleem blijft, probeer dan de browser login via het menu: %1$s Er deed zich een onbekende autorisatiefout voor. Als dit blijft, probeer dan “ Via de browser inloggen” in het menu. Autorisatie werd geweigerd. Als u zeker weet dat u de correcte gegevens heeft ingevoerd, probeer dan in te loggen in de browser via het menu. - Kon geen inlogsleutel verkrijgen. Als het probleem zich blijft herhalen; probeer dan de browser login via menu. + Kon geen inlogsleutel verkrijgen. Als het probleem zich blijft herhalen; probeer dan de browser login via menu: %1$s Laden van accountdetails mislukt: %1$s Welke Mastodonserver? Aanmelden met Pachli diff --git a/feature/login/src/main/res/values-oc/strings.xml b/feature/login/src/main/res/values-oc/strings.xml index 4f299afd63..f311c53895 100644 --- a/feature/login/src/main/res/values-oc/strings.xml +++ b/feature/login/src/main/res/values-oc/strings.xml @@ -13,7 +13,7 @@ L\'autenticacion en aquesta instància a fracassat. Se ten de se produire, ensajatz la connexion via Navigador via lo menú: %1$s S\'es produch una error d\'autorizacion pas identificada. Se ten de se produire, ensajatz la connexion via Navigador via lo menú. L\'autorizacion es estada regetada. - Fracàs de l’obtencion del geton d\'iniciacion de session. + Fracàs de l’obtencion del geton d\'iniciacion de session: %1$s Fracàs del cargament dels detalhs del compte: %1$s Quina instància ? Començar la session amb Pachli diff --git a/feature/login/src/main/res/values-pl/strings.xml b/feature/login/src/main/res/values-pl/strings.xml index c8f8d3daab..bb60ee82d1 100644 --- a/feature/login/src/main/res/values-pl/strings.xml +++ b/feature/login/src/main/res/values-pl/strings.xml @@ -13,7 +13,7 @@ Nie udało się uwierzytelnić z tą instancją. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji: %1$s Wystąpił nieokreślony błąd podczas próby autoryzacji. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji. Odmówiono autoryzacji. Jeśli jesteś pewien poprawności wprowadzonych danych, spróbuj zalogowania się za pomocą przeglądarki dostępnej w menu. - Nie udało się uzyskać tokenu logowania. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji. + Nie udało się uzyskać tokenu logowania. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji: %1$s Ładowanie informacji o koncie nie powiodło się: %1$s Jaka instancja? Zaloguj się z Pachli diff --git a/feature/login/src/main/res/values-pt-rBR/strings.xml b/feature/login/src/main/res/values-pt-rBR/strings.xml index 9d881ade39..cffc815d6d 100644 --- a/feature/login/src/main/res/values-pt-rBR/strings.xml +++ b/feature/login/src/main/res/values-pt-rBR/strings.xml @@ -7,7 +7,7 @@ Falha na autenticação com essa instância. Se isso persistir, tente Entrar pelo Navegador no menu: %1$s Ocorreu um erro de autorização não identificado. Se isso persistir, tente Entrar pelo Navegador no menu. A autorização foi negada. Se você tiver certeza de que forneceu as credenciais corretas, tente Entrar pelo Navegador no menu. - Falha ao obter um Token de login. Se isso persistir, tente Entrar pelo Navegador no menu. + Falha ao obter um Token de login. Se isso persistir, tente Entrar pelo Navegador no menu: %1$s Falha ao carregar detalhes da conta: %1$s Qual instância? Entrar com Pachli diff --git a/feature/login/src/main/res/values-pt-rPT/strings.xml b/feature/login/src/main/res/values-pt-rPT/strings.xml index 9fca17df3a..bdfdce734a 100644 --- a/feature/login/src/main/res/values-pt-rPT/strings.xml +++ b/feature/login/src/main/res/values-pt-rPT/strings.xml @@ -13,7 +13,7 @@ Erro ao autenticar com esta instância: %1$s Ocorreu um erro de autorização não identificado. Autorização negada. - Erro ao adquirir token de login. + Erro ao adquirir token de login: %1$s Erro ao carregar os detalhes da conta: %1$s Que instância\? Entrar com Mastodon diff --git a/feature/login/src/main/res/values-ru/strings.xml b/feature/login/src/main/res/values-ru/strings.xml index 1a790a47d4..4accc3b157 100644 --- a/feature/login/src/main/res/values-ru/strings.xml +++ b/feature/login/src/main/res/values-ru/strings.xml @@ -1,11 +1,11 @@ - Здесь можно ввести адрес или домен любого узла, например, mastodon.social, icosahedron.website, social.tchncs.de и других! -\n -\nЕсли у вас ещё нет аккаунта, введите адрес узла, на котором хотите зарегистрироваться, и создайте аккаунт. -\n -\n Узел — это то место, где размещён ваш аккаунт, но вы можете взаимодействовать с пользователями других узлов, как будто вы находитесь на одном сайте. -\n + Здесь можно ввести адрес или домен любого узла, например, mastodon.social, icosahedron.website, social.tchncs.de и других! +\n +\nЕсли у вас ещё нет аккаунта, введите адрес узла, на котором хотите зарегистрироваться, и создайте аккаунт. +\n +\n Узел — это то место, где размещён ваш аккаунт, но вы можете взаимодействовать с пользователями других узлов, как будто вы находитесь на одном сайте. +\n \n Чтобы получить больше информации посетите joinmastodon.org. Закрыть Вход через Браузер @@ -13,10 +13,10 @@ Ошибка аутентификации на этом узле. Если это повторится, попробуйте Вход через Браузер в меню: %1$s Произошла ошибка неопознанной авторизации. Если это повторится, попробуйте Вход через Браузер в меню. Авторизация была отклонена. - Не удалось получить токен авторизации. + Не удалось получить токен авторизации: %1$s Какой узел? Войти Что такое узел? Соединение… Войти - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-sa/strings.xml b/feature/login/src/main/res/values-sa/strings.xml index 4f5a38c443..8186e8b4b1 100644 --- a/feature/login/src/main/res/values-sa/strings.xml +++ b/feature/login/src/main/res/values-sa/strings.xml @@ -13,7 +13,7 @@ तेन विशिष्टस्थलेन प्रमाणीकरणं विफलं जातम् ।: %1$s अज्ञातः प्रमाणीकरणदोषो जातः । प्रमाणीकरणं निषिद्धम् । - सम्प्रवेशस्तोकं न लब्धम् । + सम्प्रवेशस्तोकं न लब्धम् ।: %1$s व्यक्तित्वलेखस्य विवरणानि दर्शनं विफलं जातम्: %1$s किं विशिष्टस्थलम् \? मास्टुडोनमाध्यमेन सम्प्रविश्यताम् diff --git a/feature/login/src/main/res/values-sk/strings.xml b/feature/login/src/main/res/values-sk/strings.xml index aaccfff580..42ab284a4c 100644 --- a/feature/login/src/main/res/values-sk/strings.xml +++ b/feature/login/src/main/res/values-sk/strings.xml @@ -4,7 +4,7 @@ Autentizácia servru zlyhala: %1$s Vyskytla sa neidentifikovaná chyba autorizácie. Autorizácia bola zamietnutá. - Nepodarilo sa získať prihlasovací token. + Nepodarilo sa získať prihlasovací token: %1$s Ktorý server\? Prihlásiť sa účtom Mastodon Čo je server\? diff --git a/feature/login/src/main/res/values-sl/strings.xml b/feature/login/src/main/res/values-sl/strings.xml index 93b528c01b..8140e3a4b3 100644 --- a/feature/login/src/main/res/values-sl/strings.xml +++ b/feature/login/src/main/res/values-sl/strings.xml @@ -1,20 +1,20 @@ - Tu lahko vnesete naslov ali domeno katerega koli vozlišča, na primer mastodon.social, icosahedron.website, social.tchncs.de in več! -\n -\nČe še nimate računa, lahko vnesete ime vozlišča, kateremu bi se radi pridružili, in tam ustvarite račun. -\n -\nVozlišče je ena lokacija, kjer je gostovanje vašega računa, vendar lahko preprosto komunicirate in sledite ljudem na drugih vozliščih, kot da bi bili na isti lokaciji. -\n + Tu lahko vnesete naslov ali domeno katerega koli vozlišča, na primer mastodon.social, icosahedron.website, social.tchncs.de in več! +\n +\nČe še nimate računa, lahko vnesete ime vozlišča, kateremu bi se radi pridružili, in tam ustvarite račun. +\n +\nVozlišče je ena lokacija, kjer je gostovanje vašega računa, vendar lahko preprosto komunicirate in sledite ljudem na drugih vozliščih, kot da bi bili na isti lokaciji. +\n \nVeč informacij najdete na naslovu joinmastodon.org. Zapri Vnesena je neveljavna domena Overitev s to instanco ni uspela: %1$s Prišlo je do neznane napake pri pooblastitvi. Pooblastitev je bila zavrnjena. - Ni bilo mogoče pridobiti žetona za prijavo. + Ni bilo mogoče pridobiti žetona za prijavo: %1$s Katero vozlišče\? Prijavite se z Mastodonom Kaj je instanca\? Povezovanje… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-sv/strings.xml b/feature/login/src/main/res/values-sv/strings.xml index 7b6e2cc634..60305b2dc3 100644 --- a/feature/login/src/main/res/values-sv/strings.xml +++ b/feature/login/src/main/res/values-sv/strings.xml @@ -13,7 +13,7 @@ Misslyckades att autentisera med den instansen. Om detta kvarstår, försök Logga in i webbläsaren från menyn: %1$s Ett oidentifierat auktoriseringsfel inträffade. Om detta kvarstår, försök Logga in i webbläsaren från menyn. Auktorisering nekades. Om du är säker på att du har angett rätt referenser, prova Logga in i webbläsaren från menyn. - Misslyckades med att hämta en inloggningstoken. Om detta kvarstår, försök Logga in i webbläsaren från menyn. + Misslyckades med att hämta en inloggningstoken. Om detta kvarstår, försök Logga in i webbläsaren från menyn: %1$s Kunde inte ladda kontodetaljer: %1$s Vilken instans? Logga in med Pachli diff --git a/feature/login/src/main/res/values-ta/strings.xml b/feature/login/src/main/res/values-ta/strings.xml index 21bcb08839..71e9bb100d 100644 --- a/feature/login/src/main/res/values-ta/strings.xml +++ b/feature/login/src/main/res/values-ta/strings.xml @@ -1,10 +1,10 @@ - ஏதேனும் instance-ன் முகவரியையோ அல்லது களத்தின் முகவரியையோ இங்கு உள்ளிடவும், உதாரணமாக mastodon.social, icosahedron.website, social.tchncs.de, மற்றும் \u0020மேலும்! + ஏதேனும் instance-ன் முகவரியையோ அல்லது களத்தின் முகவரியையோ இங்கு உள்ளிடவும், உதாரணமாக mastodon.social, icosahedron.website, social.tchncs.de, மற்றும் \u0020மேலும்! \n \nபயனர் கணக்கு இல்லையெனில் புதிய கணக்கிற்கான instance(களம்)-னை பதிவிடவும். நீங்கள் குறிப்பிடப்படும் களத்தில் உங்கள் கணக்கு பதிவாகும். \n -\nமேலும் இங்கு குறிப்பிடப்பட்ட ஏதேனும் ஒரு களத்தில் மட்டுமே உங்களால் கணக்கு ஆரம்பித்துக்கொள்ள இயலும் இருப்பினும் நம்மால் மற்ற களங்களில் உள்ள நண்பர்களையும் தொடர்பு கொள்ள இயலும் . +\nமேலும் இங்கு குறிப்பிடப்பட்ட ஏதேனும் ஒரு களத்தில் மட்டுமே உங்களால் கணக்கு ஆரம்பித்துக்கொள்ள இயலும் இருப்பினும் நம்மால் மற்ற களங்களில் உள்ள நண்பர்களையும் தொடர்பு கொள்ள இயலும் . \n \nமேலும் தகவல்கள் அறிய joinmastodon.org. \u0020 மூடு @@ -12,9 +12,9 @@ அந்த instance(களத்தினை)-யை அங்கீகரிப்பதில் தோல்வி: %1$s அடையாளம் தெரியாத அங்கீகார பிழை ஏற்பட்டுள்ளது. அங்கீகாரம் மறுக்கப்பட்டுள்ளது - உள்நுழைவு டோக்கனைப் பெறுவதில் தோல்வி. + உள்நுழைவு டோக்கனைப் பெறுவதில் தோல்வி: %1$s எந்த instance(களம்)? Mastodon மூலம் உள்நுழைய Instance(களம்) என்றால் என்ன? இணைக்கபடுகிறது… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-th/strings.xml b/feature/login/src/main/res/values-th/strings.xml index 593a3d58cf..2a4df50312 100644 --- a/feature/login/src/main/res/values-th/strings.xml +++ b/feature/login/src/main/res/values-th/strings.xml @@ -1,20 +1,20 @@ - ใส่ที่อยู่หรือโดเมนของ Instance ได้ที่นี่ เช่น mastodon.social icosahedron.website social.tchncs.de และ อีกมากมาย! -\n -\nถ้ายังไม่มีบัญชี สามารถใส่ชื่อ Instance ที่ต้องการจะร่วมแล้วสร้างบัญชีที่นั่น -\n -\nInstance คือที่ที่หนึ่งไว้โฮสต์บัญชีคุณ แต่คุณยังสามารถสื่อสาร ติดตามบุคคลบน Instance อื่นได้เหมือนอยู่บนไซต์เดียวกัน -\n + ใส่ที่อยู่หรือโดเมนของ Instance ได้ที่นี่ เช่น mastodon.social icosahedron.website social.tchncs.de และ อีกมากมาย! +\n +\nถ้ายังไม่มีบัญชี สามารถใส่ชื่อ Instance ที่ต้องการจะร่วมแล้วสร้างบัญชีที่นั่น +\n +\nInstance คือที่ที่หนึ่งไว้โฮสต์บัญชีคุณ แต่คุณยังสามารถสื่อสาร ติดตามบุคคลบน Instance อื่นได้เหมือนอยู่บนไซต์เดียวกัน +\n \nพบข้อมูลเพิ่มเติมได้ที่ joinmastodon.org ปิด โดเมนที่ป้อนไม่ถูกต้อง การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว: %1$s เกิดข้อผิดพลาดในการขออนุญาตสิทธิโดยไม่ทราบสาเหตุ การขออนุญาตสิทธิถูกปฏิเสธ - ไม่สามารถรับโทเค็นการเข้าสู่ระบบ + ไม่สามารถรับโทเค็นการเข้าสู่ระบบ: %1$s Instance ไหน\? เข้าสู่ระบบด้วย Mastodon Instance คือ\? กำลังเชื่อมต่อ… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-tr/strings.xml b/feature/login/src/main/res/values-tr/strings.xml index 21e19e4899..50d0a0a8e9 100644 --- a/feature/login/src/main/res/values-tr/strings.xml +++ b/feature/login/src/main/res/values-tr/strings.xml @@ -13,7 +13,7 @@ Bu sunucuda kimlik doğrulama başarısız oldu. Sorun devam ederse menüdeki Tarayıcı ile Giriş Yap seçeneğini dene: %1$s Tanımlanamayan bir yetkilendirme hatası oluştu. Sorun devam ederse menüdeki Tarayıcı ile Giriş Yap seçeneğini dene. Yetkilendirme reddedildi. Doğru hesap bilgilerini girdiğinden eminsen menüdeki Tarayıcı ile Giriş Yap seçeneğini dene. - Giriş belirteci alınırken hata oluştu. Sorun devam ederse menüdeki Tarayıcı ile Giriş Yap seçeneğini dene. + Giriş belirteci alınırken hata oluştu. Sorun devam ederse menüdeki Tarayıcı ile Giriş Yap seçeneğini dene: %1$s Hesap detayları alınırken hata: %1$s Hangi sunucu\? Pachli ile giriş yap diff --git a/feature/login/src/main/res/values-uk/strings.xml b/feature/login/src/main/res/values-uk/strings.xml index 308c11209a..ac858068e0 100644 --- a/feature/login/src/main/res/values-uk/strings.xml +++ b/feature/login/src/main/res/values-uk/strings.xml @@ -13,7 +13,7 @@ Помилка автентифікації цього сервера. Якщо проблема не зникає, спробуйте увійти через браузер з меню: %1$s Сталася помилка невпізнання авторизації. Якщо проблема не зникає, спробуйте увійти через браузер з меню. Авторизацію відхилено. Якщо ви впевнені, що вказали правильні облікові дані, спробуйте увійти через браузер з меню. - Не вдалося отримати токен входу. Якщо проблема не зникає, спробуйте увійти через браузер з меню. + Не вдалося отримати токен входу. Якщо проблема не зникає, спробуйте увійти через браузер з меню: %1$s Не вдалося завантажити подробиці облікового запису: %1$s Котрий сервер\? Увійти з Pachli diff --git a/feature/login/src/main/res/values-vi/strings.xml b/feature/login/src/main/res/values-vi/strings.xml index e35fcac5a3..bc23ff1920 100644 --- a/feature/login/src/main/res/values-vi/strings.xml +++ b/feature/login/src/main/res/values-vi/strings.xml @@ -13,7 +13,7 @@ Máy chủ này không cấp quyền truy cập: %1$s Xảy ra lỗi khi cố gắng truy cập. Truy cập bị từ chối. - Lấy token đăng nhập thất bại. + Lấy token đăng nhập thất bại: %1$s Không thể tải thông tin tài khoản: %1$s Bạn ở máy chủ nào\? Đăng nhập Mastodon diff --git a/feature/login/src/main/res/values-zh-rCN/strings.xml b/feature/login/src/main/res/values-zh-rCN/strings.xml index 57e9385ee1..862f2b7a7f 100644 --- a/feature/login/src/main/res/values-zh-rCN/strings.xml +++ b/feature/login/src/main/res/values-zh-rCN/strings.xml @@ -7,7 +7,7 @@ 未能通过该实例的身份验证。如果这个问题持续,请从菜单处尝试“在浏览器中登录”。: %1$s 发生不明授权错误。如果这个问题持续,请从菜单处尝试 “在浏览器中登录”。 授权被拒绝。如果你确定提供了正确的凭据,请从菜单处尝试“在浏览器中登录”。 - 未能获取登录令牌。如果这个问题持续,请从菜单处尝试 “在浏览器中登录”。 + 未能获取登录令牌。如果这个问题持续,请从菜单处尝试 “在浏览器中登录”。: %1$s 加载账户详情失败: %1$s 哪个实例? 用 Pachli 登录 diff --git a/feature/login/src/main/res/values-zh-rHK/strings.xml b/feature/login/src/main/res/values-zh-rHK/strings.xml index 035ad011fb..f969627276 100644 --- a/feature/login/src/main/res/values-zh-rHK/strings.xml +++ b/feature/login/src/main/res/values-zh-rHK/strings.xml @@ -6,9 +6,9 @@ 無法連接此伺服器。: %1$s 認證過程出現未知錯誤。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。 授權被拒絕。如果您確定提供了正確登入訊息,請嘗試選單中的「在瀏覽器中登錄」。 - 無法獲取登入資訊。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。 + 無法獲取登入資訊。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。: %1$s 哪一個域名? 登入 Mastodon 帳號 什麼是站點? 正在連線… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-zh-rMO/strings.xml b/feature/login/src/main/res/values-zh-rMO/strings.xml index 86adbaaa1b..3d4945e9d5 100644 --- a/feature/login/src/main/res/values-zh-rMO/strings.xml +++ b/feature/login/src/main/res/values-zh-rMO/strings.xml @@ -6,9 +6,9 @@ 無法連接此伺服器: %1$s 認證過程出現未知錯誤。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。 授權被拒絕。如果您確定提供了正確登入訊息,請嘗試選單中的「在瀏覽器中登錄」。 - 無法獲取登入資訊。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。 + 無法獲取登入資訊。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。: %1$s 哪一個域名? 登入 Mastodon 帳號 什麼是站點? 正在連線… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-zh-rSG/strings.xml b/feature/login/src/main/res/values-zh-rSG/strings.xml index cfc977c1ab..a2e85fe8ee 100644 --- a/feature/login/src/main/res/values-zh-rSG/strings.xml +++ b/feature/login/src/main/res/values-zh-rSG/strings.xml @@ -1,18 +1,18 @@ - 请输入你账号所在的 Mastodon 站点的域名,比如 pawoo.net,acg.mn,wxw.moe,等等 。 -\n -\n还没有 Mastodon 账号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的账号并授权 Pachli 登入。 -\n + 请输入你账号所在的 Mastodon 站点的域名,比如 pawoo.net,acg.mn,wxw.moe,等等 。 +\n +\n还没有 Mastodon 账号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的账号并授权 Pachli 登入。 +\n \n在 Mastodon 里,跨站互动和站内互动一样简单。可以前往 https://joinmastodon.org 了解更多信息。 关闭 该域名无效 无法连接此服务器: %1$s 认证过程出现未知错误 授权被拒绝 - 无法获取登录信息 + 无法获取登录信息: %1$s 域名 登录 Mastodon 账号 需要帮助? 正在连接… - \ No newline at end of file + diff --git a/feature/login/src/main/res/values-zh-rTW/strings.xml b/feature/login/src/main/res/values-zh-rTW/strings.xml index b63a5d0ca7..7285d406ce 100644 --- a/feature/login/src/main/res/values-zh-rTW/strings.xml +++ b/feature/login/src/main/res/values-zh-rTW/strings.xml @@ -6,7 +6,7 @@ 無法通過該伺服器的身份驗證。如果此問題持續發生,請嘗試選單中的“在瀏覽器中登錄”。: %1$s 認證過程出現未知錯誤。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。 授權被拒絕。如果您確定提供了正確登入訊息,請嘗試選單中的「在瀏覽器中登錄」。 - 無法獲取登入資訊。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。 + 無法獲取登入資訊。如果此問題持續發生,請嘗試選單中的「在瀏覽器中登錄」。: %1$s 加載賬戶詳情失敗: %1$s 哪一個域名? 登入 Mastodon 帳號 diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml index 4881d7ca4a..64686a0506 100644 --- a/feature/login/src/main/res/values/strings.xml +++ b/feature/login/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ Failed authenticating with that instance: %1$s An unidentified authorization error occurred. If this persists, try "Login in Browser" from the menu. Authorization was denied. If you\'re sure that you supplied the correct credentials, try "Login in Browser" from the menu. - Failed getting a login token. If this persists, try "Login in Browser" from the menu. + Failed getting a login token. If this persists, try "Login in Browser" from the menu: %1$s Failed loading account details: %1$s Which instance? Login with Pachli diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cada24a684..d588d283e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,7 +61,6 @@ mockito-inline = "5.2.0" mockito-kotlin = "5.4.0" moshi = "1.15.1" moshix = "0.28.0" -networkresult-calladapter = "1.0.0" okhttp = "4.12.0" okio = "3.9.1" play-services-base = "18.5.0" @@ -206,7 +205,6 @@ moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = " moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } moshix-sealed-runtime = { module = "dev.zacsweers.moshix:moshi-sealed-runtime", version.ref = "moshix" } moshix-sealed-codegen = { module = "dev.zacsweers.moshix:moshi-sealed-codegen", version.ref = "moshix" } -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" } okhttp-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okhttp" }