From efd1c8e556b879aa50a7e772b3368b0cb79ec863 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Wed, 12 Jun 2024 10:22:27 +0200 Subject: [PATCH] refactor: Use the PachliError type for ApiError (#739) In the previous code `PachliError` could correctly chain errors and generate error messages, `ApiError` didn't, which is why there was the temporary `ApiError.fmt()` extension function. Rewrite `ApiError` to implement `PachliError` so it gets these benefits and to reduce the number of different error-handling mechanisms in the code. Main changes: - `PachliError` is now an interface so it can be extended by other error interfaces. - All the `ApiError` subclasses implement `PachliError`, and can specify the error string and interpolated variables at the point of declaration. - Update `ListsRepository` and `ServerRepository` to return `PachliError` subclasses. --- app/lint-baseline.xml | 184 +++++++++--------- .../java/app/pachli/TabPreferenceActivity.kt | 2 - .../java/app/pachli/fragment/SFragment.kt | 2 +- .../app/pachli/core/common/PachliError.kt | 67 ++++--- .../core/data/repository/ListsRepository.kt | 19 +- .../core/data/repository/ServerRepository.kt | 22 +-- core/network/lint-baseline.xml | 2 +- .../kotlin/app/pachli/core/network/Server.kt | 16 +- .../core/network/model/nodeinfo/NodeInfo.kt | 9 +- .../network/retrofit/apiresult/ApiResult.kt | 128 ++++++++---- .../src/main/res/values-ar/strings.xml | 6 +- .../src/main/res/values-de/strings.xml | 6 +- .../src/main/res/values-en-rGB/strings.xml | 5 +- .../src/main/res/values-es/strings.xml | 6 +- .../src/main/res/values-fi/strings.xml | 6 +- .../src/main/res/values-fr/strings.xml | 6 +- .../src/main/res/values-in/strings.xml | 6 +- .../src/main/res/values-it/strings.xml | 6 +- .../src/main/res/values-ja/strings.xml | 6 +- .../src/main/res/values-kab/strings.xml | 4 +- .../src/main/res/values-nl/strings.xml | 5 +- .../src/main/res/values-pt-rBR/strings.xml | 6 +- .../src/main/res/values-sv/strings.xml | 5 +- core/network/src/main/res/values/strings.xml | 4 + .../retrofit/apiresult/ApiResultCallTest.kt | 9 +- .../network/retrofit/apiresult/ApiTest.kt | 26 +-- .../pachli/core/ui/BackgroundMessageView.kt | 5 + .../ui/extensions/PachliErrorExtensions.kt | 36 ++++ .../core/ui/extensions/ThrowableExtensions.kt | 13 +- core/ui/src/main/res/values-ar/strings.xml | 3 - core/ui/src/main/res/values-de/strings.xml | 3 - .../ui/src/main/res/values-en-rGB/strings.xml | 2 - core/ui/src/main/res/values-es/strings.xml | 4 - core/ui/src/main/res/values-fi/strings.xml | 3 - core/ui/src/main/res/values-fr/strings.xml | 3 - core/ui/src/main/res/values-in/strings.xml | 3 - core/ui/src/main/res/values-it/strings.xml | 3 - core/ui/src/main/res/values-ja/strings.xml | 3 - core/ui/src/main/res/values-kab/strings.xml | 3 +- core/ui/src/main/res/values-nl/strings.xml | 3 - .../ui/src/main/res/values-pt-rBR/strings.xml | 3 - core/ui/src/main/res/values-sv/strings.xml | 3 - core/ui/src/main/res/values/strings.xml | 4 - .../feature/lists/AccountsInListFragment.kt | 9 +- .../app/pachli/feature/lists/ListsActivity.kt | 36 +--- .../feature/lists/ListsForAccountFragment.kt | 2 +- .../feature/lists/ListsForAccountViewModel.kt | 13 +- .../pachli/feature/lists/ListsViewModel.kt | 18 +- 48 files changed, 411 insertions(+), 327 deletions(-) create mode 100644 core/ui/src/main/kotlin/app/pachli/core/ui/extensions/PachliErrorExtensions.kt diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 79238f25c..79a4921cb 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -124,7 +124,7 @@ errorLine2=" ^"> @@ -168,7 +168,7 @@ errorLine2=" ^"> @@ -179,7 +179,7 @@ errorLine2=" ^"> @@ -190,7 +190,7 @@ errorLine2=" ^"> @@ -212,7 +212,7 @@ errorLine2=" ^"> @@ -223,7 +223,7 @@ errorLine2=" ^"> @@ -234,7 +234,7 @@ errorLine2=" ^"> @@ -245,7 +245,7 @@ errorLine2=" ^"> @@ -267,7 +267,7 @@ errorLine2=" ^"> @@ -278,7 +278,7 @@ errorLine2=" ^"> @@ -289,21 +289,21 @@ errorLine2=" ^"> + + + + - - - - @@ -322,7 +322,7 @@ errorLine2=" ^"> @@ -333,7 +333,7 @@ errorLine2=" ^"> @@ -344,7 +344,7 @@ errorLine2=" ^"> @@ -355,7 +355,7 @@ errorLine2=" ^"> @@ -366,7 +366,7 @@ errorLine2=" ^"> @@ -377,7 +377,7 @@ errorLine2=" ^"> @@ -388,7 +388,7 @@ errorLine2=" ^"> @@ -399,7 +399,7 @@ errorLine2=" ^"> @@ -410,7 +410,7 @@ errorLine2=" ^"> @@ -421,7 +421,7 @@ errorLine2=" ^"> @@ -432,7 +432,7 @@ errorLine2=" ^"> @@ -443,7 +443,7 @@ errorLine2=" ^"> @@ -454,7 +454,7 @@ errorLine2=" ^"> @@ -465,7 +465,7 @@ errorLine2=" ^"> @@ -476,7 +476,7 @@ errorLine2=" ^"> @@ -509,7 +509,7 @@ errorLine2=" ^"> @@ -520,7 +520,7 @@ errorLine2=" ^"> @@ -531,7 +531,7 @@ errorLine2=" ^"> @@ -542,7 +542,7 @@ errorLine2=" ^"> @@ -553,7 +553,7 @@ errorLine2=" ^"> @@ -564,7 +564,7 @@ errorLine2=" ^"> @@ -575,7 +575,7 @@ errorLine2=" ^"> @@ -586,7 +586,7 @@ errorLine2=" ^"> @@ -597,7 +597,7 @@ errorLine2=" ^"> @@ -608,7 +608,7 @@ errorLine2=" ^"> @@ -619,7 +619,7 @@ errorLine2=" ^"> @@ -630,7 +630,7 @@ errorLine2=" ^"> @@ -641,7 +641,7 @@ errorLine2=" ^"> @@ -652,7 +652,7 @@ errorLine2=" ^"> @@ -663,7 +663,7 @@ errorLine2=" ^"> @@ -674,7 +674,7 @@ errorLine2=" ^"> @@ -685,7 +685,7 @@ errorLine2=" ^"> @@ -696,7 +696,7 @@ errorLine2=" ^"> @@ -729,7 +729,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -740,7 +740,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -751,7 +751,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -762,7 +762,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -773,7 +773,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1290,7 +1290,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1301,7 +1301,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -1312,7 +1312,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1323,7 +1323,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1334,7 +1334,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1345,7 +1345,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1356,7 +1356,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1367,7 +1367,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1378,7 +1378,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1389,7 +1389,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1400,7 +1400,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1411,7 +1411,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1422,7 +1422,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -1433,7 +1433,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1444,7 +1444,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -1455,7 +1455,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -1466,7 +1466,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -1477,7 +1477,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -1488,7 +1488,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -1499,7 +1499,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -1510,7 +1510,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1521,7 +1521,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1532,7 +1532,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -1543,7 +1543,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1554,7 +1554,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1565,7 +1565,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1576,7 +1576,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -1587,7 +1587,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1598,7 +1598,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1609,7 +1609,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1620,7 +1620,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1631,7 +1631,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> diff --git a/app/src/main/java/app/pachli/TabPreferenceActivity.kt b/app/src/main/java/app/pachli/TabPreferenceActivity.kt index 2ba80d618..61e73e90e 100644 --- a/app/src/main/java/app/pachli/TabPreferenceActivity.kt +++ b/app/src/main/java/app/pachli/TabPreferenceActivity.kt @@ -65,7 +65,6 @@ import java.util.regex.Pattern import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import timber.log.Timber @AndroidEntryPoint class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { @@ -332,7 +331,6 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { selectListBinding.progressBar.hide() dialog.dismiss() Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_LONG).show() - Timber.w(it.throwable, "failed to load lists") } } } diff --git a/app/src/main/java/app/pachli/fragment/SFragment.kt b/app/src/main/java/app/pachli/fragment/SFragment.kt index 8e9812a4e..fe77e650b 100644 --- a/app/src/main/java/app/pachli/fragment/SFragment.kt +++ b/app/src/main/java/app/pachli/fragment/SFragment.kt @@ -129,7 +129,7 @@ abstract class SFragment : Fragment(), StatusActionListener val msg = getString( R.string.server_repository_error, accountManager.activeAccount!!.domain, - it.msg(requireContext()), + it.fmt(requireContext()), ) Timber.e(msg) try { diff --git a/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt b/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt index ae1ecb2f5..adfa08501 100644 --- a/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt +++ b/core/common/src/main/kotlin/app/pachli/core/common/PachliError.kt @@ -19,36 +19,41 @@ package app.pachli.core.common import android.content.Context import androidx.annotation.StringRes +import app.pachli.core.common.string.unicodeWrap /** - * Base class for errors throughout the app. + * Interface for errors throughout the app. * * Derive new error class hierarchies for different components using a sealed * class hierarchy like so: * * ```kotlin * sealed class Error( - * @StringRes resourceId: Int, - * vararg formatArgs: String, - * source: PachliError? = null, - * ) : PachliError(resourceId, *formatArgs, source = source) { + * @StringRes override val resourceId: Int, + * override val formatArgs: Array, + * cause: PachliError? = null, + * ) : PachliError { * data object SomeProblem : Error(R.string.error_some_problem) * data class OutOfRange(val input: Int) : Error( - * R.string.error_out_of_range + * R.string.error_out_of_range, // "Value %1$d is out of range" * input, * ) - * data class Fetch(val url: String, val e: PachliError) : Error( - * R.string.error_fetch, + * data class Fetch(val url: String, val cause: PachliError) : Error( + * R.string.error_fetch, // "Could not fetch %1$s: %2$s" * url, - * source = e, + * cause = cause, * ) * } * ``` * - * In this example `SomeProblem` represents an error with no additional context, - * `OtherProblem` is an error relating to a URL and the URL will be included in - * the error message, and `WrappedError` represents an error that wraps another - * error that was the actual cause. + * In this example `SomeProblem` represents an error with no additional context. + * + * `OutOfRange` is an error relating to a single value with no underlying cause. + * The value (`input`) will be inserted in the string at `%1$s`. + * + * `Fetch` is an error relating to a URL with an underlying cause. The URL will be + * included in the error message at `%1$s`, and the string representation of the + * cause will be included at `%2$s`. * * Possible string resources for those errors would be: * @@ -57,23 +62,27 @@ import androidx.annotation.StringRes * Value %1$d is out of range * Could not fetch %1$s: %2$s * ``` - * - * In that last example the `url` parameter will be interpolated as the first - * placeholder and the error message from the error passed as the `source` - * parameter will be interpolated as the second placeholder. - * - * @property resourceId String resource ID for the error message - * @property formatArgs 0 or more arguments to interpolate in to the string resource - * @property source (optional) The underlying error that caused this error */ -open class PachliError( - @StringRes private val resourceId: Int, - private vararg val formatArgs: String, - val source: PachliError? = null, -) { - fun msg(context: Context): String { +interface PachliError { + /** String resource ID for the error message. */ + @get:StringRes + val resourceId: Int + + /** Arguments to be interpolated in to the string from [resourceId]. */ + val formatArgs: Array + + /** + * The cause of this error. If present the string representation of `cause` + * will be set as the last format argument when formatting [resourceId]. + */ + val cause: PachliError? + + /** + * @return A localised, unicode-wrapped error message for this error. + */ + fun fmt(context: Context): String { val args = mutableListOf(*formatArgs) - source?.let { args.add(it.msg(context)) } - return context.getString(resourceId, *args.toTypedArray()) + cause?.let { args.add(it.fmt(context)) } + return context.getString(resourceId, *args.toTypedArray()).unicodeWrap() } } diff --git a/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt b/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt index b10138557..e83f8c9d5 100644 --- a/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt +++ b/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt @@ -17,6 +17,7 @@ package app.pachli.core.data.repository +import app.pachli.core.common.PachliError import app.pachli.core.network.model.MastoList import app.pachli.core.network.model.TimelineAccount import app.pachli.core.network.model.UserListRepliesPolicy @@ -36,26 +37,26 @@ interface HasListId { } /** Errors that can be returned from this repository */ -interface ListsError : ApiError { +interface ListsError : PachliError { @JvmInline - value class Create(private val error: ApiError) : ListsError, ApiError by error + value class Create(private val error: ApiError) : ListsError, PachliError by error @JvmInline - value class Retrieve(private val error: ApiError) : ListsError, ApiError by error + value class Retrieve(private val error: ApiError) : ListsError, PachliError by error @JvmInline - value class Update(private val error: ApiError) : ListsError, ApiError by error + value class Update(private val error: ApiError) : ListsError, PachliError by error @JvmInline - value class Delete(private val error: ApiError) : ListsError, ApiError by error + value class Delete(private val error: ApiError) : ListsError, PachliError by error - data class GetListsWithAccount(val accountId: String, private val error: ApiError) : ListsError, ApiError by error + data class GetListsWithAccount(val accountId: String, private val error: ApiError) : ListsError, PachliError by error - data class GetAccounts(override val listId: String, private val error: ApiError) : ListsError, HasListId, ApiError by error + data class GetAccounts(override val listId: String, private val error: ApiError) : ListsError, HasListId, PachliError by error - data class AddAccounts(override val listId: String, private val error: ApiError) : ListsError, HasListId, ApiError by error + data class AddAccounts(override val listId: String, private val error: ApiError) : ListsError, HasListId, PachliError by error - data class DeleteAccounts(override val listId: String, private val error: ApiError) : ListsError, HasListId, ApiError by error + data class DeleteAccounts(override val listId: String, private val error: ApiError) : ListsError, HasListId, PachliError by error } interface ListsRepository { diff --git a/core/data/src/main/kotlin/app/pachli/core/data/repository/ServerRepository.kt b/core/data/src/main/kotlin/app/pachli/core/data/repository/ServerRepository.kt index 498596f76..b14a9464c 100644 --- a/core/data/src/main/kotlin/app/pachli/core/data/repository/ServerRepository.kt +++ b/core/data/src/main/kotlin/app/pachli/core/data/repository/ServerRepository.kt @@ -119,13 +119,14 @@ class ServerRepository @Inject constructor( } sealed class Error( - @StringRes resourceId: Int, - vararg formatArgs: String, - source: PachliError? = null, - ) : PachliError(resourceId, *formatArgs, source = source) { + @StringRes override val resourceId: Int, + override val formatArgs: Array = emptyArray(), + override val cause: PachliError? = null, + ) : PachliError { + data class GetWellKnownNodeInfo(val throwable: Throwable) : Error( R.string.server_repository_error_get_well_known_node_info, - throwable.localizedMessage, + throwable.localizedMessage?.let { arrayOf(it) }.orEmpty(), ) data object UnsupportedSchema : Error( @@ -134,24 +135,23 @@ class ServerRepository @Inject constructor( data class GetNodeInfo(val url: String, val throwable: Throwable) : Error( R.string.server_repository_error_get_node_info, - url, - throwable.localizedMessage, + arrayOf(url, throwable.localizedMessage ?: ""), ) data class ValidateNodeInfo(val url: String, val error: NodeInfo.Error) : Error( R.string.server_repository_error_validate_node_info, - url, - source = error, + arrayOf(url), + cause = error, ) data class GetInstanceInfoV1(val throwable: Throwable) : Error( R.string.server_repository_error_get_instance_info, - throwable.localizedMessage, + throwable.localizedMessage?.let { arrayOf(it) }.orEmpty(), ) data class Capabilities(val error: Server.Error) : Error( R.string.server_repository_error_capabilities, - source = error, + cause = error, ) } } diff --git a/core/network/lint-baseline.xml b/core/network/lint-baseline.xml index fb7b14703..a84701647 100644 --- a/core/network/lint-baseline.xml +++ b/core/network/lint-baseline.xml @@ -1,4 +1,4 @@ - + diff --git a/core/network/src/main/kotlin/app/pachli/core/network/Server.kt b/core/network/src/main/kotlin/app/pachli/core/network/Server.kt index 35323d739..a2904452b 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/Server.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/Server.kt @@ -17,7 +17,6 @@ package app.pachli.core.network -import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.Companion.PRIVATE import app.pachli.core.common.PachliError @@ -296,16 +295,13 @@ data class Server( } /** Errors that can occur when processing server capabilities */ - sealed class Error( - @StringRes resourceId: Int, - vararg formatArgs: String, - ) : PachliError(resourceId, *formatArgs) { + sealed interface Error : PachliError { /** Could not parse the server's version string */ - data class UnparseableVersion(val version: String, val throwable: Throwable) : Error( - R.string.server_error_unparseable_version, - version, - throwable.localizedMessage, - ) + data class UnparseableVersion(val version: String, val throwable: Throwable) : Error { + override val resourceId = R.string.server_error_unparseable_version + override val formatArgs: Array = arrayOf(version, throwable.localizedMessage ?: "") + override val cause: PachliError? = null + } } } diff --git a/core/network/src/main/kotlin/app/pachli/core/network/model/nodeinfo/NodeInfo.kt b/core/network/src/main/kotlin/app/pachli/core/network/model/nodeinfo/NodeInfo.kt index 831dc9c8d..4395114da 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/model/nodeinfo/NodeInfo.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/model/nodeinfo/NodeInfo.kt @@ -77,10 +77,11 @@ data class NodeInfo(val software: Software) { } sealed class Error( - @StringRes resourceId: Int, - vararg formatArgs: String, - source: PachliError? = null, - ) : PachliError(resourceId, *formatArgs, source = source) { + @StringRes override val resourceId: Int, + ) : PachliError { + override val formatArgs = emptyArray() + override val cause: PachliError? = null + data object NoSoftwareBlock : Error(R.string.node_info_error_no_software) data object NoSoftwareName : Error(R.string.node_info_error_no_software_name) data object NoSoftwareVersion : Error(R.string.node_info_error_no_software_version) diff --git a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResult.kt b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResult.kt index daf3e1f3d..50ff6b7b0 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResult.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResult.kt @@ -17,6 +17,10 @@ package app.pachli.core.network.retrofit.apiresult +import androidx.annotation.StringRes +import app.pachli.core.common.PachliError +import app.pachli.core.network.R +import app.pachli.core.network.extensions.getServerErrorMessage import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result @@ -45,50 +49,57 @@ data class ApiResponse( /** * A failed response from an API call. + * + * @param resourceId String resource ID of the error message. + * @param throwable The [Throwable] that caused this error. The server + * message (if it exists), or [Throwable.getLocalizedMessage] will be + * interpolated in to this string at `%1$s`. */ -interface ApiError { - // This has to be Throwable, not Exception, because Retrofit exposes - // errors as Throwable - val throwable: Throwable +sealed class ApiError( + @StringRes override val resourceId: Int, + val throwable: Throwable, +) : PachliError { + override val formatArgs = ( + throwable.getServerErrorMessage() ?: throwable.localizedMessage + )?.let { arrayOf(it) }.orEmpty() + override val cause: PachliError? = null companion object { - fun from(exception: Throwable): ApiError { - return when (exception) { - is HttpException -> when (exception.code()) { - in 400..499 -> ClientError.from(exception) - in 500..599 -> ServerError.from(exception) - else -> Unknown(exception) + fun from(throwable: Throwable): ApiError { + return when (throwable) { + is HttpException -> when (throwable.code()) { + in 400..499 -> ClientError.from(throwable) + in 500..599 -> ServerError.from(throwable) + else -> Unknown(throwable) } - is JsonDataException -> JsonParse(exception) - is IOException -> IO(exception) - else -> Unknown(exception) + is JsonDataException -> JsonParseError(throwable) + is IOException -> IoError(throwable) + else -> Unknown(throwable) } } } - data class Unknown(override val throwable: Throwable) : ApiError + data class Unknown(val exception: Throwable) : ApiError( + R.string.error_generic_fmt, + exception, + ) } -sealed interface HttpError : ApiError { - override val throwable: HttpException - - /** - * The error message for this error, one of (in preference order): - * - * - The error body of the response that created this error - * - The throwable.message - * - Literal string "Unknown" - */ - val message - get() = throwable.response()?.errorBody()?.string() ?: throwable.message() ?: "Unknown" -} +sealed class HttpError( + @StringRes override val resourceId: Int, + open val exception: HttpException, +) : ApiError(resourceId, exception) /** 4xx errors */ -sealed interface ClientError : HttpError { +sealed class ClientError( + @StringRes override val resourceId: Int, + exception: HttpException, +) : HttpError(resourceId, exception) { companion object { fun from(exception: HttpException): ClientError { return when (exception.code()) { + 400 -> BadRequest(exception) 401 -> Unauthorized(exception) 404 -> NotFound(exception) 410 -> Gone(exception) @@ -97,14 +108,32 @@ sealed interface ClientError : HttpError { } } - data class Unauthorized(override val throwable: HttpException) : ClientError - data class NotFound(override val throwable: HttpException) : ClientError - data class Gone(override val throwable: HttpException) : ClientError - data class UnknownClientError(override val throwable: HttpException) : ClientError + /** 400 Bad request */ + data class BadRequest(override val exception: HttpException) : + ClientError(R.string.error_generic_fmt, exception) + + /** 401 Unauthorized */ + data class Unauthorized(override val exception: HttpException) : + ClientError(R.string.error_generic_fmt, exception) + + /** 404 Not found */ + data class NotFound(override val exception: HttpException) : + ClientError(R.string.error_404_not_found_fmt, exception) + + /** 410 Gone */ + data class Gone(override val exception: HttpException) : + ClientError(R.string.error_generic_fmt, exception) + + /** All other 4xx client errors */ + data class UnknownClientError(override val exception: HttpException) : + ClientError(R.string.error_generic_fmt, exception) } /** 5xx errors */ -sealed interface ServerError : HttpError { +sealed class ServerError( + @StringRes override val resourceId: Int, + exception: HttpException, +) : HttpError(resourceId, exception) { companion object { fun from(exception: HttpException): ServerError { return when (exception.code()) { @@ -117,15 +146,32 @@ sealed interface ServerError : HttpError { } } - data class Internal(override val throwable: HttpException) : ServerError - data class NotImplemented(override val throwable: HttpException) : ServerError - data class BadGateway(override val throwable: HttpException) : ServerError - data class ServiceUnavailable(override val throwable: HttpException) : ServerError - data class UnknownServerError(override val throwable: HttpException) : ServerError + /** 500 Internal error */ + data class Internal(override val exception: HttpException) : + ServerError(R.string.error_generic_fmt, exception) + + /** 501 Not implemented */ + data class NotImplemented(override val exception: HttpException) : + ServerError(R.string.error_404_not_found_fmt, exception) + + /** 502 Bad gateway */ + data class BadGateway(override val exception: HttpException) : + ServerError(R.string.error_generic_fmt, exception) + + /** 503 Service unavailable */ + data class ServiceUnavailable(override val exception: HttpException) : + ServerError(R.string.error_generic_fmt, exception) + + /** All other 5xx server errors */ + data class UnknownServerError(override val exception: HttpException) : + ServerError(R.string.error_generic_fmt, exception) } -data class JsonParse(override val throwable: JsonDataException) : ApiError -sealed interface NetworkError : ApiError -data class IO(override val throwable: Exception) : NetworkError + +data class JsonParseError(val exception: JsonDataException) : + ApiError(R.string.error_json_data_fmt, exception) + +data class IoError(val exception: IOException) : + ApiError(R.string.error_network_fmt, exception) /** * Creates an [ApiResult] from a [Response]. diff --git a/core/network/src/main/res/values-ar/strings.xml b/core/network/src/main/res/values-ar/strings.xml index a6b3daec9..6bfc567cf 100644 --- a/core/network/src/main/res/values-ar/strings.xml +++ b/core/network/src/main/res/values-ar/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + وقع خطأ: %s + خادمك لا يدعم هذه الميزة: %1$s + وقع خطأ في الشبكة: %s + diff --git a/core/network/src/main/res/values-de/strings.xml b/core/network/src/main/res/values-de/strings.xml index a6b3daec9..390147c11 100644 --- a/core/network/src/main/res/values-de/strings.xml +++ b/core/network/src/main/res/values-de/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Es ist ein Fehler aufgetreten: %s + Dein Server unterstützt diese Funktion nicht: %1$s + Ein Netzwerkfehler ist aufgetreten: %s + diff --git a/core/network/src/main/res/values-en-rGB/strings.xml b/core/network/src/main/res/values-en-rGB/strings.xml index a6b3daec9..8fc09b1b3 100644 --- a/core/network/src/main/res/values-en-rGB/strings.xml +++ b/core/network/src/main/res/values-en-rGB/strings.xml @@ -1,2 +1,5 @@ - \ No newline at end of file + + An error occurred: %s + Your server does not support this feature: %1$s + diff --git a/core/network/src/main/res/values-es/strings.xml b/core/network/src/main/res/values-es/strings.xml index 65371aed8..c531b01cd 100644 --- a/core/network/src/main/res/values-es/strings.xml +++ b/core/network/src/main/res/values-es/strings.xml @@ -4,4 +4,8 @@ versión del programa faltante, vacía o en blanco no se pudo analizar \"%1$s\" como una versión: %2$s no hay bloque sobre el programa - \ No newline at end of file + Ha ocurrido un error: %s + Su servidor no soporta esta función: %1$s + Tu servidor devolvió una respuesta inválida: %1$s + Ha ocurrido un error de red: %s + diff --git a/core/network/src/main/res/values-fi/strings.xml b/core/network/src/main/res/values-fi/strings.xml index a6b3daec9..f6f08f58a 100644 --- a/core/network/src/main/res/values-fi/strings.xml +++ b/core/network/src/main/res/values-fi/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Tapahtui virhe: %s + Palvelimesi ei tue tätä ominaisuutta: %1$s + Tapahtui verkkovirhe: %s + diff --git a/core/network/src/main/res/values-fr/strings.xml b/core/network/src/main/res/values-fr/strings.xml index a6b3daec9..dee920f55 100644 --- a/core/network/src/main/res/values-fr/strings.xml +++ b/core/network/src/main/res/values-fr/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Une erreur s\'est produite : %s + Votre serveur ne prend pas en charge cette fonctionnalité: %1$s + Une erreur réseau s\'est produite : %s + diff --git a/core/network/src/main/res/values-in/strings.xml b/core/network/src/main/res/values-in/strings.xml index a6b3daec9..6f5510fd5 100644 --- a/core/network/src/main/res/values-in/strings.xml +++ b/core/network/src/main/res/values-in/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Terjadi error: %s + Server Anda tidak mendukung fitur ini: %1$s + Jaringan error: %s + diff --git a/core/network/src/main/res/values-it/strings.xml b/core/network/src/main/res/values-it/strings.xml index a6b3daec9..79973eae4 100644 --- a/core/network/src/main/res/values-it/strings.xml +++ b/core/network/src/main/res/values-it/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Si è verificato un errore: %s + Il tuo server non supporta questa feature: %1$s + Si è verificato un errore di rete: %s + diff --git a/core/network/src/main/res/values-ja/strings.xml b/core/network/src/main/res/values-ja/strings.xml index a6b3daec9..d04b24b23 100644 --- a/core/network/src/main/res/values-ja/strings.xml +++ b/core/network/src/main/res/values-ja/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + エラーが発生しました: %s + あなたのサーバーはこの機能をサポートしていません: %1$s + ネットワーク エラーが発生しました: %s + diff --git a/core/network/src/main/res/values-kab/strings.xml b/core/network/src/main/res/values-kab/strings.xml index a6b3daec9..7e755e1ba 100644 --- a/core/network/src/main/res/values-kab/strings.xml +++ b/core/network/src/main/res/values-kab/strings.xml @@ -1,2 +1,4 @@ - \ No newline at end of file + + Tella-d tuccḍa: %s + diff --git a/core/network/src/main/res/values-nl/strings.xml b/core/network/src/main/res/values-nl/strings.xml index c75dcd51b..c6e0cf050 100644 --- a/core/network/src/main/res/values-nl/strings.xml +++ b/core/network/src/main/res/values-nl/strings.xml @@ -4,4 +4,7 @@ software naam mist, is leeg of blanco software versie mist, is leeg of blanco kon \"%1$s\" niet verwerken als een versie: %2$s - \ No newline at end of file + Er deed zich een fout voor: %s + Je server beschikt niet over ondersteuning voor deze feature: %1$s + Er deed zich een netwerkfout voor: %s + diff --git a/core/network/src/main/res/values-pt-rBR/strings.xml b/core/network/src/main/res/values-pt-rBR/strings.xml index a6b3daec9..c1e3db390 100644 --- a/core/network/src/main/res/values-pt-rBR/strings.xml +++ b/core/network/src/main/res/values-pt-rBR/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Ocorreu um erro: %s + Tua instância não suporta este recurso: %1$s + Ocorreu um erro de rede: %s + diff --git a/core/network/src/main/res/values-sv/strings.xml b/core/network/src/main/res/values-sv/strings.xml index f5b90cb94..314b0e353 100644 --- a/core/network/src/main/res/values-sv/strings.xml +++ b/core/network/src/main/res/values-sv/strings.xml @@ -4,4 +4,7 @@ innehöll inget mjukvarublock mjukvarunamnet saknas, är tomt eller blankt Kunde inte analysera \"%1$s\" som en version: %2$s - \ No newline at end of file + Ett fel har uppstått: %s + Din server stöder inte denna funktion: %1$s + Ett nätverksfel har uppstått: %s + diff --git a/core/network/src/main/res/values/strings.xml b/core/network/src/main/res/values/strings.xml index 7329ad856..80cecd6bd 100644 --- a/core/network/src/main/res/values/strings.xml +++ b/core/network/src/main/res/values/strings.xml @@ -21,4 +21,8 @@ software version is missing, empty, or blank could not parse \"%1$s\" as a version: %2$s + An error occurred: %s + Your server does not support this feature: %1$s + Your server returned an invalid response: %1$s + A network error occurred: %s diff --git a/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResultCallTest.kt b/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResultCallTest.kt index eb278f217..699853d20 100644 --- a/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResultCallTest.kt +++ b/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiResultCallTest.kt @@ -89,11 +89,10 @@ class ApiResultCallTest { override fun onResponse(call: Call>, response: Response>) { val error = response.body()?.getError() as? ClientError.NotFound assertThat(error).isInstanceOf(ClientError.NotFound::class.java) - assertThat(error?.message).isEqualTo("not found") - val throwable = error?.throwable - assertThat(throwable).isInstanceOf(HttpException::class.java) - assertThat(throwable?.code()).isEqualTo(404) + val exception = error?.exception + assertThat(exception).isInstanceOf(HttpException::class.java) + assertThat(exception?.code()).isEqualTo(404) } override fun onFailure(call: Call>, t: Throwable) { @@ -107,7 +106,7 @@ class ApiResultCallTest { @Test fun `should parse call with IOException as ApiResult-failure`() { - val error = Err(IO(IOException())) + val error = Err(IoError(IOException())) networkApiResultCall.enqueue( object : Callback> { diff --git a/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiTest.kt b/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiTest.kt index 3b46df0bd..e8348897c 100644 --- a/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiTest.kt +++ b/core/network/src/test/kotlin/app/pachli/core/network/retrofit/apiresult/ApiTest.kt @@ -140,8 +140,8 @@ class ApiTest { val error = responseObject as? ServerError.Internal assertThat(error).isInstanceOf(ServerError.Internal::class.java) - assertThat(error?.throwable?.code()).isEqualTo(500) - assertThat(error?.throwable?.message()).isEqualTo("Server Error") + assertThat(error?.exception?.code()).isEqualTo(500) + assertThat(error?.exception?.message()).isEqualTo("Server Error") } @Test @@ -155,8 +155,8 @@ class ApiTest { val error = responseObject as? ServerError.Internal assertThat(error).isInstanceOf(ServerError.Internal::class.java) - assertThat(error?.throwable?.code()).isEqualTo(500) - assertThat(error?.throwable?.message()).isEqualTo("Server Error") + assertThat(error?.exception?.code()).isEqualTo(500) + assertThat(error?.exception?.message()).isEqualTo("Server Error") } @Test @@ -167,9 +167,9 @@ class ApiTest { api.getSiteAsync() } - val error = responseObject.getError() as? IO + val error = responseObject.getError() as? IoError - assertThat(error).isInstanceOf(IO::class.java) + assertThat(error).isInstanceOf(IoError::class.java) } @Test @@ -177,9 +177,9 @@ class ApiTest { mockWebServer.enqueue(MockResponse().apply { socketPolicy = SocketPolicy.DISCONNECT_AFTER_REQUEST }) val responseObject = runBlocking { api.getSiteSync() } - val error = responseObject.getError() as? IO + val error = responseObject.getError() as? IoError - assertThat(error).isInstanceOf(IO::class.java) + assertThat(error).isInstanceOf(IoError::class.java) } @Test @@ -189,9 +189,9 @@ class ApiTest { mockWebServer.enqueue(response) val responseObject = api.getSitesAsync().getError() - val error = responseObject as? JsonParse + val error = responseObject as? JsonParseError - assertThat(error).isInstanceOf(JsonParse::class.java) + assertThat(error).isInstanceOf(JsonParseError::class.java) } @Test @@ -201,11 +201,11 @@ class ApiTest { val responseObject = api.getSitesAsync().getError() - val error = responseObject as? IO + val error = responseObject as? IoError // Moshi reports invalid JSON as an IoException wrapping a JsonEncodingException - assertThat(error).isInstanceOf(IO::class.java) - assertThat(error?.throwable).isInstanceOf(JsonEncodingException::class.java) + assertThat(error).isInstanceOf(IoError::class.java) + assertThat(error?.exception).isInstanceOf(JsonEncodingException::class.java) } @Test diff --git a/core/ui/src/main/kotlin/app/pachli/core/ui/BackgroundMessageView.kt b/core/ui/src/main/kotlin/app/pachli/core/ui/BackgroundMessageView.kt index 0e6356ed6..c3db41cc7 100644 --- a/core/ui/src/main/kotlin/app/pachli/core/ui/BackgroundMessageView.kt +++ b/core/ui/src/main/kotlin/app/pachli/core/ui/BackgroundMessageView.kt @@ -32,6 +32,7 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import app.pachli.core.common.PachliError import app.pachli.core.common.extensions.visible import app.pachli.core.ui.databinding.ViewBackgroundMessageBinding import app.pachli.core.ui.extensions.getDrawableRes @@ -99,6 +100,10 @@ class BackgroundMessageView @JvmOverloads constructor( setup(throwable.getDrawableRes(), throwable.getErrorString(context), listener) } + fun setup(error: PachliError, listener: ((v: View) -> Unit)? = null) { + setup(error.getDrawableRes(), error.fmt(context), listener) + } + fun setup(message: BackgroundMessage, listener: ((v: View) -> Unit)? = null) { setup(message.drawableRes, message.stringRes, listener) } diff --git a/core/ui/src/main/kotlin/app/pachli/core/ui/extensions/PachliErrorExtensions.kt b/core/ui/src/main/kotlin/app/pachli/core/ui/extensions/PachliErrorExtensions.kt new file mode 100644 index 000000000..49c26d44b --- /dev/null +++ b/core/ui/src/main/kotlin/app/pachli/core/ui/extensions/PachliErrorExtensions.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +package app.pachli.core.ui.extensions + +import androidx.annotation.DrawableRes +import app.pachli.core.common.PachliError +import app.pachli.core.network.retrofit.apiresult.ClientError +import app.pachli.core.network.retrofit.apiresult.HttpError +import app.pachli.core.network.retrofit.apiresult.IoError +import app.pachli.core.ui.R + +/** @return A drawable resource to accompany the error message for this [PachliError]. */ +@DrawableRes +fun PachliError.getDrawableRes(): Int = when (this) { + is IoError -> R.drawable.errorphant_offline + is HttpError -> when (this) { + is ClientError.NotFound -> R.drawable.elephant_friend_empty + else -> R.drawable.errorphant_offline + } + else -> R.drawable.errorphant_offline +} diff --git a/core/ui/src/main/kotlin/app/pachli/core/ui/extensions/ThrowableExtensions.kt b/core/ui/src/main/kotlin/app/pachli/core/ui/extensions/ThrowableExtensions.kt index 0d631bb41..4f152ce4d 100644 --- a/core/ui/src/main/kotlin/app/pachli/core/ui/extensions/ThrowableExtensions.kt +++ b/core/ui/src/main/kotlin/app/pachli/core/ui/extensions/ThrowableExtensions.kt @@ -18,7 +18,9 @@ package app.pachli.core.ui.extensions import android.content.Context +import androidx.annotation.DrawableRes import app.pachli.core.common.string.unicodeWrap +import app.pachli.core.network.R as NR import app.pachli.core.network.extensions.getServerErrorMessage import app.pachli.core.ui.R import com.squareup.moshi.JsonDataException @@ -26,6 +28,7 @@ import java.io.IOException import retrofit2.HttpException /** @return A drawable resource to accompany the error message for this throwable */ +@DrawableRes fun Throwable.getDrawableRes(): Int = when (this) { is IOException -> R.drawable.errorphant_offline is HttpException -> { @@ -41,15 +44,15 @@ fun Throwable.getDrawableRes(): Int = when (this) { /** @return A unicode-wrapped string error message for this throwable */ fun Throwable.getErrorString(context: Context): String = ( getServerErrorMessage() ?: when (this) { - is IOException -> String.format(context.getString(R.string.error_network_fmt), localizedMessage) + is IOException -> String.format(context.getString(NR.string.error_network_fmt), localizedMessage) is HttpException -> { if (code() == 404) { - String.format(context.getString(R.string.error_404_not_found_fmt), localizedMessage) + String.format(context.getString(NR.string.error_404_not_found_fmt), localizedMessage) } else { - String.format(context.getString(R.string.error_generic_fmt), localizedMessage) + String.format(context.getString(NR.string.error_generic_fmt), localizedMessage) } } - is JsonDataException -> String.format(context.getString(R.string.error_json_data_fmt), localizedMessage) - else -> String.format(context.getString(R.string.error_generic_fmt), localizedMessage) + is JsonDataException -> String.format(context.getString(NR.string.error_json_data_fmt), localizedMessage) + else -> String.format(context.getString(NR.string.error_generic_fmt), localizedMessage) } ).unicodeWrap() diff --git a/core/ui/src/main/res/values-ar/strings.xml b/core/ui/src/main/res/values-ar/strings.xml index cf4faea65..9ad73019c 100644 --- a/core/ui/src/main/res/values-ar/strings.xml +++ b/core/ui/src/main/res/values-ar/strings.xml @@ -1,9 +1,6 @@ حدث خطأ في الشبكة! يرجى التحقق من اتصالك ثم أعد المحاولة! - وقع خطأ في الشبكة: %s - وقع خطأ: %s - خادمك لا يدعم هذه الميزة: %1$s وقع هناك خطأ. لا شيء هنا. أعد المحاولة diff --git a/core/ui/src/main/res/values-de/strings.xml b/core/ui/src/main/res/values-de/strings.xml index 0e2acd355..c6ce86808 100644 --- a/core/ui/src/main/res/values-de/strings.xml +++ b/core/ui/src/main/res/values-de/strings.xml @@ -1,9 +1,6 @@ Ein Netzwerkfehler ist aufgetreten. Bitte überprüfe deine Internetverbindung und versuche es erneut. - Ein Netzwerkfehler ist aufgetreten: %s - Es ist ein Fehler aufgetreten: %s - Dein Server unterstützt diese Funktion nicht: %1$s Ein Fehler ist aufgetreten. Hier ist nichts. Erneut versuchen diff --git a/core/ui/src/main/res/values-en-rGB/strings.xml b/core/ui/src/main/res/values-en-rGB/strings.xml index 181933188..421501210 100644 --- a/core/ui/src/main/res/values-en-rGB/strings.xml +++ b/core/ui/src/main/res/values-en-rGB/strings.xml @@ -1,7 +1,5 @@ A network error occurred! Please check your connection and try again! - An error occurred: %s - Your server does not support this feature: %1$s An error occurred. diff --git a/core/ui/src/main/res/values-es/strings.xml b/core/ui/src/main/res/values-es/strings.xml index 18a948e71..cf91904b8 100644 --- a/core/ui/src/main/res/values-es/strings.xml +++ b/core/ui/src/main/res/values-es/strings.xml @@ -1,9 +1,6 @@ Ha ocurrido un error de red. Por favor, comprueba tu conexión e inténtalo de nuevo. - Ha ocurrido un error de red: %s - Ha ocurrido un error: %s - Su servidor no soporta esta función: %1$s Ha ocurrido un error. Nada aquí. Reintentar @@ -11,6 +8,5 @@ Perfil Más Recargar - Tu servidor devolvió una respuesta inválida: %1$s \u0020(🔗 %s) diff --git a/core/ui/src/main/res/values-fi/strings.xml b/core/ui/src/main/res/values-fi/strings.xml index 146c0fe9f..0b9d02c7d 100644 --- a/core/ui/src/main/res/values-fi/strings.xml +++ b/core/ui/src/main/res/values-fi/strings.xml @@ -1,9 +1,6 @@ Verkkovirhe. Tarkista yhteytesi ja yritä uudelleen. - Tapahtui verkkovirhe: %s - Tapahtui virhe: %s - Palvelimesi ei tue tätä ominaisuutta: %1$s Tapahtui virhe. Täällä ei ole mitään. Yritä uudelleen diff --git a/core/ui/src/main/res/values-fr/strings.xml b/core/ui/src/main/res/values-fr/strings.xml index 1ff446828..5ea49a515 100644 --- a/core/ui/src/main/res/values-fr/strings.xml +++ b/core/ui/src/main/res/values-fr/strings.xml @@ -1,9 +1,6 @@ Une erreur réseau s’est produite ! Veuillez vérifier votre connexion puis réessayez ! - Une erreur réseau s\'est produite : %s - Une erreur s\'est produite : %s - Votre serveur ne prend pas en charge cette fonctionnalité: %1$s Une erreur s’est produite. Rien ici. Réessayer diff --git a/core/ui/src/main/res/values-in/strings.xml b/core/ui/src/main/res/values-in/strings.xml index 88f1ddb96..9d840c90b 100644 --- a/core/ui/src/main/res/values-in/strings.xml +++ b/core/ui/src/main/res/values-in/strings.xml @@ -1,9 +1,6 @@ Terjadi kesalahan pada jaringan! Harap periksa koneksi Anda dan coba lagi! - Jaringan error: %s - Terjadi error: %s - Server Anda tidak mendukung fitur ini: %1$s Terjadi kesalahan. Tidak ada apa pun disini. Coba lagi diff --git a/core/ui/src/main/res/values-it/strings.xml b/core/ui/src/main/res/values-it/strings.xml index 4d8501af4..c70b7d5cc 100644 --- a/core/ui/src/main/res/values-it/strings.xml +++ b/core/ui/src/main/res/values-it/strings.xml @@ -1,9 +1,6 @@ Si è verificato un errore di rete. Per favore controlla la tua connessione e riprova. - Si è verificato un errore di rete: %s - Si è verificato un errore: %s - Il tuo server non supporta questa feature: %1$s Si è verificato un errore. Qui non c\'è nulla. Riprova diff --git a/core/ui/src/main/res/values-ja/strings.xml b/core/ui/src/main/res/values-ja/strings.xml index e254b7e8b..cf392b0ca 100644 --- a/core/ui/src/main/res/values-ja/strings.xml +++ b/core/ui/src/main/res/values-ja/strings.xml @@ -1,9 +1,6 @@ ネットワークエラーが発生しました。接続を確認してもう一度試してください。 - ネットワーク エラーが発生しました: %s - エラーが発生しました: %s - あなたのサーバーはこの機能をサポートしていません: %1$s エラーが発生しました。 何もありません。 再試行 diff --git a/core/ui/src/main/res/values-kab/strings.xml b/core/ui/src/main/res/values-kab/strings.xml index e34721f39..974294f13 100644 --- a/core/ui/src/main/res/values-kab/strings.xml +++ b/core/ui/src/main/res/values-kab/strings.xml @@ -5,7 +5,6 @@ Ɛreḍ tikkelt-nniḍen Amaɣnu Ugar - Tella-d tuccḍa: %s Smiren Immed - \ No newline at end of file + diff --git a/core/ui/src/main/res/values-nl/strings.xml b/core/ui/src/main/res/values-nl/strings.xml index 0bc42f5ca..d2f292bae 100644 --- a/core/ui/src/main/res/values-nl/strings.xml +++ b/core/ui/src/main/res/values-nl/strings.xml @@ -1,9 +1,6 @@ Er deed zich een netwerkfout voor. Controleer je verbinding en probeer opnieuw. - Er deed zich een netwerkfout voor: %s - Er deed zich een fout voor: %s - Je server beschikt niet over ondersteuning voor deze feature: %1$s Er deed zich een fout voor. Hier is niets. Opnieuw proberen diff --git a/core/ui/src/main/res/values-pt-rBR/strings.xml b/core/ui/src/main/res/values-pt-rBR/strings.xml index 283742416..fd3e68945 100644 --- a/core/ui/src/main/res/values-pt-rBR/strings.xml +++ b/core/ui/src/main/res/values-pt-rBR/strings.xml @@ -1,9 +1,6 @@ Ocorreu um erro de rede. Por favor, verifique tua Internet e tente novamente. - Ocorreu um erro de rede: %s - Ocorreu um erro: %s - Tua instância não suporta este recurso: %1$s Um erro ocorreu. Nada aqui. Tentar novamente diff --git a/core/ui/src/main/res/values-sv/strings.xml b/core/ui/src/main/res/values-sv/strings.xml index 944c30fb5..66e01b9b1 100644 --- a/core/ui/src/main/res/values-sv/strings.xml +++ b/core/ui/src/main/res/values-sv/strings.xml @@ -1,9 +1,6 @@ Ett nätverksfel uppstod. Kontrollera din anslutning och försök igen. - Ett nätverksfel har uppstått: %s - Ett fel har uppstått: %s - Din server stöder inte denna funktion: %1$s Ett fel har uppstått. Ingenting här. Försök igen diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index f1be1dcf8..765821396 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -1,10 +1,6 @@ A network error occurred. Please check your connection and try again. - A network error occurred: %s - An error occurred: %s - Your server does not support this feature: %1$s - Your server returned an invalid response: %1$s An error occurred. Nothing here. Retry diff --git a/feature/lists/src/main/kotlin/app/pachli/feature/lists/AccountsInListFragment.kt b/feature/lists/src/main/kotlin/app/pachli/feature/lists/AccountsInListFragment.kt index 6584318f7..333dabf3d 100644 --- a/feature/lists/src/main/kotlin/app/pachli/feature/lists/AccountsInListFragment.kt +++ b/feature/lists/src/main/kotlin/app/pachli/feature/lists/AccountsInListFragment.kt @@ -34,6 +34,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import app.pachli.core.activity.emojify import app.pachli.core.activity.loadAvatar +import app.pachli.core.common.PachliError import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.viewBinding @@ -141,7 +142,7 @@ class AccountsInListFragment : DialogFragment() { launch { viewModel.errors.collect { - handleError(it.throwable) + handleError(it) } } } @@ -172,7 +173,7 @@ class AccountsInListFragment : DialogFragment() { if (it is Accounts.Loaded) adapter.submitList(it.accounts) }.onFailure { binding.messageView.show() - handleError(it.throwable) + handleError(it) } } @@ -196,11 +197,11 @@ class AccountsInListFragment : DialogFragment() { } }.onFailure { Timber.w(it.throwable, "Error searching for accounts in list") - handleError(it.throwable) + handleError(it) } } - private fun handleError(error: Throwable) { + private fun handleError(error: PachliError) { binding.messageView.show() binding.messageView.setup(error) { binding.messageView.hide() diff --git a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsActivity.kt b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsActivity.kt index cc5797642..0f6ffaa09 100644 --- a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsActivity.kt +++ b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsActivity.kt @@ -43,14 +43,12 @@ import app.pachli.core.activity.extensions.startActivityWithDefaultTransition import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.viewBinding -import app.pachli.core.common.string.unicodeWrap import app.pachli.core.data.repository.Lists +import app.pachli.core.data.repository.ListsError import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle import app.pachli.core.navigation.TimelineActivityIntent import app.pachli.core.network.model.MastoList import app.pachli.core.network.model.UserListRepliesPolicy -import app.pachli.core.network.retrofit.apiresult.ApiError -import app.pachli.core.network.retrofit.apiresult.NetworkError import app.pachli.core.ui.BackgroundMessage import app.pachli.core.ui.extensions.await import app.pachli.feature.lists.databinding.ActivityListsBinding @@ -113,29 +111,7 @@ class ListsActivity : BaseActivity(), MenuProvider { lifecycleScope.launch { viewModel.errors.collect { error -> - when (error) { - is Error.Create -> showMessage( - String.format( - getString(R.string.error_create_list_fmt), - error.title.unicodeWrap(), - error.throwable.message.unicodeWrap(), - ), - ) - is Error.Delete -> showMessage( - String.format( - getString(R.string.error_delete_list_fmt), - error.title.unicodeWrap(), - error.throwable.message.unicodeWrap(), - ), - ) - is Error.Update -> showMessage( - String.format( - getString(R.string.error_rename_list_fmt), - error.title.unicodeWrap(), - error.throwable.message.unicodeWrap(), - ), - ) - } + showMessage(error.fmt(this@ListsActivity)) } } @@ -220,17 +196,13 @@ class ListsActivity : BaseActivity(), MenuProvider { if (result == AlertDialog.BUTTON_POSITIVE) viewModel.deleteList(list.id, list.title) } - private fun bind(state: Result) { + private fun bind(state: Result) { state.onFailure { binding.listsRecycler.hide() binding.messageView.show() binding.swipeRefreshLayout.isRefreshing = false - if (it is NetworkError) { - binding.messageView.setup(BackgroundMessage.Network()) { viewModel.refresh() } - } else { - binding.messageView.setup(BackgroundMessage.GenericError()) { viewModel.refresh() } - } + binding.messageView.setup(it) { viewModel.refresh() } } state.onSuccess { lists -> diff --git a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountFragment.kt b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountFragment.kt index 845f841e4..f34325314 100644 --- a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountFragment.kt +++ b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountFragment.kt @@ -155,7 +155,7 @@ class ListsForAccountFragment : DialogFragment() { binding.listsView.hide() binding.messageView.apply { show() - setup(it.throwable) { + setup(it) { viewModel.refresh() load() } diff --git a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountViewModel.kt b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountViewModel.kt index b4a7990a6..ef6f1be16 100644 --- a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountViewModel.kt +++ b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsForAccountViewModel.kt @@ -24,7 +24,6 @@ import app.pachli.core.data.repository.Lists import app.pachli.core.data.repository.ListsError import app.pachli.core.data.repository.ListsRepository import app.pachli.core.network.model.MastoList -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 @@ -67,7 +66,7 @@ class ListsForAccountViewModel @AssistedInject constructor( private val _listsWithMembership = MutableStateFlow>(Ok(ListsWithMembership.Loading)) val listsWithMembership = _listsWithMembership.asStateFlow() - private val _errors = Channel() + private val _errors = Channel() val errors = _errors.receiveAsFlow() private val listsWithMembershipMap = mutableMapOf() @@ -164,21 +163,21 @@ class ListsForAccountViewModel @AssistedInject constructor( * Marker for errors that can be part of the [Result] in the * [ListsForAccountViewModel.listsWithMembership] flow */ - sealed interface FlowError : ApiError + sealed interface FlowError : Error /** Asynchronous errors from network operations */ sealed interface Error : ListsError { /** Failed to fetch lists, or lists containing a particular account */ @JvmInline - value class GetListsWithAccount(val error: ListsError.GetListsWithAccount) : FlowError, ListsError by error + value class GetListsWithAccount(private val error: ListsError.GetListsWithAccount) : FlowError, Error, ListsError by error @JvmInline - value class Retrieve(val error: ListsError.Retrieve) : FlowError, ListsError by error + value class Retrieve(private val error: ListsError.Retrieve) : FlowError, Error, ListsError by error @JvmInline - value class AddAccounts(val error: ListsError.AddAccounts) : Error, HasListId by error, ListsError by error + value class AddAccounts(private val error: ListsError.AddAccounts) : Error, HasListId by error, ListsError by error @JvmInline - value class DeleteAccounts(val error: ListsError.DeleteAccounts) : Error, HasListId by error, ListsError by error + value class DeleteAccounts(private val error: ListsError.DeleteAccounts) : Error, HasListId by error, ListsError by error } } diff --git a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsViewModel.kt b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsViewModel.kt index c11790a22..d32c99c41 100644 --- a/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsViewModel.kt +++ b/feature/lists/src/main/kotlin/app/pachli/feature/lists/ListsViewModel.kt @@ -17,8 +17,10 @@ package app.pachli.feature.lists +import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.pachli.core.common.string.unicodeWrap import app.pachli.core.data.repository.ListsError import app.pachli.core.data.repository.ListsRepository import app.pachli.core.network.model.UserListRepliesPolicy @@ -32,14 +34,20 @@ import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -sealed interface Error : ListsError { - val title: String +sealed class Error( + @StringRes override val resourceId: Int, + override val formatArgs: Array, + override val cause: ListsError? = null, +) : ListsError { - data class Create(override val title: String, private val error: ListsError.Create) : Error, ListsError by error + data class Create(val title: String, override val cause: ListsError.Create) : + Error(R.string.error_create_list_fmt, arrayOf(title.unicodeWrap()), cause) - data class Delete(override val title: String, private val error: ListsError.Delete) : Error, ListsError by error + data class Delete(val title: String, override val cause: ListsError.Delete) : + Error(R.string.error_delete_list_fmt, arrayOf(title.unicodeWrap()), cause) - data class Update(override val title: String, private val error: ListsError.Update) : Error, ListsError by error + data class Update(val title: String, override val cause: ListsError.Update) : + Error(R.string.error_rename_list_fmt, arrayOf(title.unicodeWrap()), cause) } @HiltViewModel