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.
This commit is contained in:
Nik Clayton 2024-06-12 10:22:27 +02:00 committed by GitHub
parent 7bf8c382e1
commit efd1c8e556
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 411 additions and 327 deletions

View File

@ -124,7 +124,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="133"
line="132"
column="5"/>
</issue>
@ -168,7 +168,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-sl/strings.xml"
line="180"
line="179"
column="5"/>
</issue>
@ -179,7 +179,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="194"
line="193"
column="5"/>
</issue>
@ -190,7 +190,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-bn-rIN/strings.xml"
line="201"
line="200"
column="5"/>
</issue>
@ -212,7 +212,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-sl/strings.xml"
line="214"
line="213"
column="5"/>
</issue>
@ -223,7 +223,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="236"
line="235"
column="5"/>
</issue>
@ -234,7 +234,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="237"
line="236"
column="5"/>
</issue>
@ -245,7 +245,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-bn-rIN/strings.xml"
line="242"
line="241"
column="5"/>
</issue>
@ -267,7 +267,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-hi/strings.xml"
line="270"
line="269"
column="5"/>
</issue>
@ -278,7 +278,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="273"
line="272"
column="5"/>
</issue>
@ -289,21 +289,21 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="276"
column="5"/>
</issue>
<issue
id="MissingQuantity"
message="For locale &quot;cs&quot; (Czech) the following quantity should also be defined: `many` (e.g. &quot;10.0 dne&quot;)"
errorLine1=" &lt;plurals name=&quot;favs&quot;>"
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="277"
column="5"/>
</issue>
<issue
id="MissingQuantity"
message="For locale &quot;cs&quot; (Czech) the following quantity should also be defined: `many` (e.g. &quot;10.0 dne&quot;)"
errorLine1=" &lt;plurals name=&quot;favs&quot;>"
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="278"
column="5"/>
</issue>
<issue
id="MissingQuantity"
message="For locale &quot;cs&quot; (Czech) the following quantity should also be defined: `many` (e.g. &quot;10.0 dne&quot;)"
@ -311,7 +311,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="283"
line="282"
column="5"/>
</issue>
@ -322,7 +322,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="302"
line="301"
column="5"/>
</issue>
@ -333,7 +333,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="313"
line="312"
column="5"/>
</issue>
@ -344,7 +344,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="327"
line="326"
column="5"/>
</issue>
@ -355,7 +355,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="332"
line="331"
column="5"/>
</issue>
@ -366,7 +366,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="337"
line="336"
column="5"/>
</issue>
@ -377,7 +377,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-bg/strings.xml"
line="369"
line="368"
column="5"/>
</issue>
@ -388,7 +388,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="369"
line="368"
column="5"/>
</issue>
@ -399,7 +399,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="397"
line="396"
column="5"/>
</issue>
@ -410,7 +410,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="401"
line="400"
column="5"/>
</issue>
@ -421,7 +421,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="405"
line="404"
column="5"/>
</issue>
@ -432,7 +432,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="409"
line="408"
column="5"/>
</issue>
@ -443,7 +443,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="417"
line="416"
column="5"/>
</issue>
@ -454,7 +454,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="422"
line="421"
column="5"/>
</issue>
@ -465,7 +465,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-ca/strings.xml"
line="425"
line="424"
column="5"/>
</issue>
@ -476,7 +476,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="427"
line="426"
column="5"/>
</issue>
@ -509,7 +509,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="34"
line="33"
column="44"/>
</issue>
@ -520,7 +520,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-tr/strings.xml"
line="40"
line="39"
column="50"/>
</issue>
@ -531,7 +531,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-tr/strings.xml"
line="40"
line="39"
column="46"/>
</issue>
@ -542,7 +542,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="71"
line="70"
column="38"/>
</issue>
@ -553,7 +553,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="73"
line="72"
column="46"/>
</issue>
@ -564,7 +564,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="79"
line="78"
column="44"/>
</issue>
@ -575,7 +575,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="102"
line="101"
column="45"/>
</issue>
@ -586,7 +586,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="106"
line="105"
column="44"/>
</issue>
@ -597,7 +597,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="107"
line="106"
column="49"/>
</issue>
@ -608,7 +608,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="110"
line="109"
column="38"/>
</issue>
@ -619,7 +619,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="125"
line="124"
column="70"/>
</issue>
@ -630,7 +630,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="158"
line="157"
column="78"/>
</issue>
@ -641,7 +641,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="164"
line="163"
column="65"/>
</issue>
@ -652,7 +652,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="200"
line="199"
column="32"/>
</issue>
@ -663,7 +663,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="268"
line="267"
column="43"/>
</issue>
@ -674,7 +674,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="396"
line="395"
column="86"/>
</issue>
@ -685,7 +685,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-tr/strings.xml"
line="504"
line="503"
column="294"/>
</issue>
@ -696,7 +696,7 @@
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="526"
line="525"
column="51"/>
</issue>
@ -729,7 +729,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="101"
line="100"
column="5"/>
</issue>
@ -740,7 +740,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="288"
line="287"
column="5"/>
</issue>
@ -751,7 +751,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="339"
line="338"
column="5"/>
</issue>
@ -762,7 +762,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="458"
line="457"
column="5"/>
</issue>
@ -773,7 +773,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="633"
line="632"
column="5"/>
</issue>
@ -1290,7 +1290,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="76"
line="75"
column="13"/>
</issue>
@ -1301,7 +1301,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="82"
line="81"
column="13"/>
</issue>
@ -1312,7 +1312,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="103"
line="102"
column="13"/>
</issue>
@ -1323,7 +1323,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="111"
line="110"
column="13"/>
</issue>
@ -1334,7 +1334,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="145"
line="144"
column="13"/>
</issue>
@ -1345,7 +1345,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="204"
line="203"
column="13"/>
</issue>
@ -1356,7 +1356,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="225"
line="224"
column="13"/>
</issue>
@ -1367,7 +1367,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="226"
line="225"
column="13"/>
</issue>
@ -1378,7 +1378,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="249"
line="248"
column="13"/>
</issue>
@ -1389,7 +1389,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="278"
line="277"
column="13"/>
</issue>
@ -1400,7 +1400,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="349"
line="348"
column="13"/>
</issue>
@ -1411,7 +1411,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="389"
line="388"
column="13"/>
</issue>
@ -1422,7 +1422,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="425"
line="424"
column="13"/>
</issue>
@ -1433,7 +1433,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="428"
line="427"
column="13"/>
</issue>
@ -1444,7 +1444,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="429"
line="428"
column="13"/>
</issue>
@ -1455,7 +1455,7 @@
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="430"
line="429"
column="13"/>
</issue>
@ -1466,7 +1466,7 @@
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="431"
line="430"
column="13"/>
</issue>
@ -1477,7 +1477,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="432"
line="431"
column="13"/>
</issue>
@ -1488,7 +1488,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="444"
line="443"
column="13"/>
</issue>
@ -1499,7 +1499,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="445"
line="444"
column="13"/>
</issue>
@ -1510,7 +1510,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="496"
line="495"
column="13"/>
</issue>
@ -1521,7 +1521,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="497"
line="496"
column="13"/>
</issue>
@ -1532,7 +1532,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="508"
line="507"
column="13"/>
</issue>
@ -1543,7 +1543,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="509"
line="508"
column="13"/>
</issue>
@ -1554,7 +1554,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="510"
line="509"
column="13"/>
</issue>
@ -1565,7 +1565,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="512"
line="511"
column="13"/>
</issue>
@ -1576,7 +1576,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="550"
line="549"
column="13"/>
</issue>
@ -1587,7 +1587,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="591"
line="590"
column="13"/>
</issue>
@ -1598,7 +1598,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="597"
line="596"
column="13"/>
</issue>
@ -1609,7 +1609,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="625"
line="624"
column="13"/>
</issue>
@ -1620,7 +1620,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="639"
line="638"
column="13"/>
</issue>
@ -1631,7 +1631,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="652"
line="651"
column="13"/>
</issue>

View File

@ -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")
}
}
}

View File

@ -129,7 +129,7 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
val msg = getString(
R.string.server_repository_error,
accountManager.activeAccount!!.domain,
it.msg(requireContext()),
it.fmt(requireContext()),
)
Timber.e(msg)
try {

View File

@ -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<out String>,
* 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
* <string name="error_out_of_range">Value %1$d is out of range</string>
* <string name="error_fetch">Could not fetch %1$s: %2$s</string>
* ```
*
* 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<out String>
/**
* 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()
}
}

View File

@ -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 {

View File

@ -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<out String> = emptyArray<String>(),
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,
)
}
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0)" variant="all" version="8.3.0">
<issues format="6" by="lint 8.3.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.2)" variant="all" version="8.3.2">
</issues>

View File

@ -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<String> = arrayOf(version, throwable.localizedMessage ?: "")
override val cause: PachliError? = null
}
}
}

View File

@ -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<String>()
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)

View File

@ -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<out T>(
/**
* 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].

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">وقع خطأ: %s</string>
<string name="error_404_not_found_fmt">خادمك لا يدعم هذه الميزة: %1$s</string>
<string name="error_network_fmt">وقع خطأ في الشبكة: %s</string>
</resources>

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">Es ist ein Fehler aufgetreten: %s</string>
<string name="error_404_not_found_fmt">Dein Server unterstützt diese Funktion nicht: %1$s</string>
<string name="error_network_fmt">Ein Netzwerkfehler ist aufgetreten: %s</string>
</resources>

View File

@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">An error occurred: %s</string>
<string name="error_404_not_found_fmt">Your server does not support this feature: %1$s</string>
</resources>

View File

@ -4,4 +4,8 @@
<string name="node_info_error_no_software_version">versión del programa faltante, vacía o en blanco</string>
<string name="server_error_unparseable_version">no se pudo analizar \"%1$s\" como una versión: %2$s</string>
<string name="node_info_error_no_software">no hay bloque sobre el programa</string>
<string name="error_generic_fmt">Ha ocurrido un error: %s</string>
<string name="error_404_not_found_fmt">Su servidor no soporta esta función: %1$s</string>
<string name="error_json_data_fmt">Tu servidor devolvió una respuesta inválida: %1$s</string>
<string name="error_network_fmt">Ha ocurrido un error de red: %s</string>
</resources>

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">Tapahtui virhe: %s</string>
<string name="error_404_not_found_fmt">Palvelimesi ei tue tätä ominaisuutta: %1$s</string>
<string name="error_network_fmt">Tapahtui verkkovirhe: %s</string>
</resources>

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">Une erreur s\'est produite: %s</string>
<string name="error_404_not_found_fmt">Votre serveur ne prend pas en charge cette fonctionnalité: %1$s</string>
<string name="error_network_fmt">Une erreur réseau s\'est produite: %s</string>
</resources>

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">Terjadi error: %s</string>
<string name="error_404_not_found_fmt">Server Anda tidak mendukung fitur ini: %1$s</string>
<string name="error_network_fmt">Jaringan error: %s</string>
</resources>

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">Si è verificato un errore: %s</string>
<string name="error_404_not_found_fmt">Il tuo server non supporta questa feature: %1$s</string>
<string name="error_network_fmt">Si è verificato un errore di rete: %s</string>
</resources>

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">エラーが発生しました: %s</string>
<string name="error_404_not_found_fmt">あなたのサーバーはこの機能をサポートしていません: %1$s</string>
<string name="error_network_fmt">ネットワーク エラーが発生しました: %s</string>
</resources>

View File

@ -1,2 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">Tella-d tuccḍa: %s</string>
</resources>

View File

@ -4,4 +4,7 @@
<string name="node_info_error_no_software_name">software naam mist, is leeg of blanco</string>
<string name="node_info_error_no_software_version">software versie mist, is leeg of blanco</string>
<string name="server_error_unparseable_version">kon \"%1$s\" niet verwerken als een versie: %2$s</string>
<string name="error_generic_fmt">Er deed zich een fout voor: %s</string>
<string name="error_404_not_found_fmt">Je server beschikt niet over ondersteuning voor deze feature: %1$s</string>
<string name="error_network_fmt">Er deed zich een netwerkfout voor: %s</string>
</resources>

View File

@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="error_generic_fmt">Ocorreu um erro: %s</string>
<string name="error_404_not_found_fmt">Tua instância não suporta este recurso: %1$s</string>
<string name="error_network_fmt">Ocorreu um erro de rede: %s</string>
</resources>

View File

@ -4,4 +4,7 @@
<string name="node_info_error_no_software">innehöll inget mjukvarublock</string>
<string name="node_info_error_no_software_name">mjukvarunamnet saknas, är tomt eller blankt</string>
<string name="server_error_unparseable_version">Kunde inte analysera \"%1$s\" som en version: %2$s</string>
<string name="error_generic_fmt">Ett fel har uppstått: %s</string>
<string name="error_404_not_found_fmt">Din server stöder inte denna funktion: %1$s</string>
<string name="error_network_fmt">Ett nätverksfel har uppstått: %s</string>
</resources>

View File

@ -21,4 +21,8 @@
<string name="node_info_error_no_software_version">software version is missing, empty, or blank</string>
<string name="server_error_unparseable_version">could not parse \"%1$s\" as a version: %2$s</string>
<string name="error_generic_fmt">An error occurred: %s</string>
<string name="error_404_not_found_fmt">Your server does not support this feature: %1$s</string>
<string name="error_json_data_fmt">Your server returned an invalid response: %1$s</string>
<string name="error_network_fmt">A network error occurred: %s</string>
</resources>

View File

@ -89,11 +89,10 @@ class ApiResultCallTest {
override fun onResponse(call: Call<ApiResult<String>>, response: Response<ApiResult<String>>) {
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<ApiResult<String>>, 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<ApiResult<String>> {

View File

@ -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

View File

@ -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)
}

View File

@ -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 <http://www.gnu.org/licenses>.
*/
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
}

View File

@ -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()

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">حدث خطأ في الشبكة! يرجى التحقق من اتصالك ثم أعد المحاولة!</string>
<string name="error_network_fmt">وقع خطأ في الشبكة: %s</string>
<string name="error_generic_fmt">وقع خطأ: %s</string>
<string name="error_404_not_found_fmt">خادمك لا يدعم هذه الميزة: %1$s</string>
<string name="error_generic">وقع هناك خطأ.</string>
<string name="message_empty">لا شيء هنا.</string>
<string name="action_retry">أعد المحاولة</string>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Ein Netzwerkfehler ist aufgetreten. Bitte überprüfe deine Internetverbindung und versuche es erneut.</string>
<string name="error_network_fmt">Ein Netzwerkfehler ist aufgetreten: %s</string>
<string name="error_generic_fmt">Es ist ein Fehler aufgetreten: %s</string>
<string name="error_404_not_found_fmt">Dein Server unterstützt diese Funktion nicht: %1$s</string>
<string name="error_generic">Ein Fehler ist aufgetreten.</string>
<string name="message_empty">Hier ist nichts.</string>
<string name="action_retry">Erneut versuchen</string>

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">A network error occurred! Please check your connection and try again!</string>
<string name="error_generic_fmt">An error occurred: %s</string>
<string name="error_404_not_found_fmt">Your server does not support this feature: %1$s</string>
<string name="error_generic">An error occurred.</string>
</resources>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_network">Ha ocurrido un error de red. Por favor, comprueba tu conexión e inténtalo de nuevo.</string>
<string name="error_network_fmt">Ha ocurrido un error de red: %s</string>
<string name="error_generic_fmt">Ha ocurrido un error: %s</string>
<string name="error_404_not_found_fmt">Su servidor no soporta esta función: %1$s</string>
<string name="error_generic">Ha ocurrido un error.</string>
<string name="message_empty">Nada aquí.</string>
<string name="action_retry">Reintentar</string>
@ -11,6 +8,5 @@
<string name="action_view_profile">Perfil</string>
<string name="action_more">Más</string>
<string name="action_refresh">Recargar</string>
<string name="error_json_data_fmt">Tu servidor devolvió una respuesta inválida: %1$s</string>
<string name="url_domain_notifier">\u0020(🔗 %s)</string>
</resources>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Verkkovirhe. Tarkista yhteytesi ja yritä uudelleen.</string>
<string name="error_network_fmt">Tapahtui verkkovirhe: %s</string>
<string name="error_generic_fmt">Tapahtui virhe: %s</string>
<string name="error_404_not_found_fmt">Palvelimesi ei tue tätä ominaisuutta: %1$s</string>
<string name="error_generic">Tapahtui virhe.</string>
<string name="message_empty">Täällä ei ole mitään.</string>
<string name="action_retry">Yritä uudelleen</string>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Une erreur réseau sest produite ! Veuillez vérifier votre connexion puis réessayez !</string>
<string name="error_network_fmt">Une erreur réseau s\'est produite: %s</string>
<string name="error_generic_fmt">Une erreur s\'est produite: %s</string>
<string name="error_404_not_found_fmt">Votre serveur ne prend pas en charge cette fonctionnalité: %1$s</string>
<string name="error_generic">Une erreur sest produite.</string>
<string name="message_empty">Rien ici.</string>
<string name="action_retry">Réessayer</string>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Terjadi kesalahan pada jaringan! Harap periksa koneksi Anda dan coba lagi!</string>
<string name="error_network_fmt">Jaringan error: %s</string>
<string name="error_generic_fmt">Terjadi error: %s</string>
<string name="error_404_not_found_fmt">Server Anda tidak mendukung fitur ini: %1$s</string>
<string name="error_generic">Terjadi kesalahan.</string>
<string name="message_empty">Tidak ada apa pun disini.</string>
<string name="action_retry">Coba lagi</string>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Si è verificato un errore di rete. Per favore controlla la tua connessione e riprova.</string>
<string name="error_network_fmt">Si è verificato un errore di rete: %s</string>
<string name="error_generic_fmt">Si è verificato un errore: %s</string>
<string name="error_404_not_found_fmt">Il tuo server non supporta questa feature: %1$s</string>
<string name="error_generic">Si è verificato un errore.</string>
<string name="message_empty">Qui non c\'è nulla.</string>
<string name="action_retry">Riprova</string>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">ネットワークエラーが発生しました。接続を確認してもう一度試してください。</string>
<string name="error_network_fmt">ネットワーク エラーが発生しました: %s</string>
<string name="error_generic_fmt">エラーが発生しました: %s</string>
<string name="error_404_not_found_fmt">あなたのサーバーはこの機能をサポートしていません: %1$s</string>
<string name="error_generic">エラーが発生しました。</string>
<string name="message_empty">何もありません。</string>
<string name="action_retry">再試行</string>

View File

@ -5,7 +5,6 @@
<string name="action_retry">Ɛreḍ tikkelt-nniḍen</string>
<string name="action_view_profile">Amaɣnu</string>
<string name="action_more">Ugar</string>
<string name="error_generic_fmt">Tella-d tuccḍa: %s</string>
<string name="action_refresh">Smiren</string>
<string name="button_done">Immed</string>
</resources>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Er deed zich een netwerkfout voor. Controleer je verbinding en probeer opnieuw.</string>
<string name="error_network_fmt">Er deed zich een netwerkfout voor: %s</string>
<string name="error_generic_fmt">Er deed zich een fout voor: %s</string>
<string name="error_404_not_found_fmt">Je server beschikt niet over ondersteuning voor deze feature: %1$s</string>
<string name="error_generic">Er deed zich een fout voor.</string>
<string name="message_empty">Hier is niets.</string>
<string name="action_retry">Opnieuw proberen</string>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Ocorreu um erro de rede. Por favor, verifique tua Internet e tente novamente.</string>
<string name="error_network_fmt">Ocorreu um erro de rede: %s</string>
<string name="error_generic_fmt">Ocorreu um erro: %s</string>
<string name="error_404_not_found_fmt">Tua instância não suporta este recurso: %1$s</string>
<string name="error_generic">Um erro ocorreu.</string>
<string name="message_empty">Nada aqui.</string>
<string name="action_retry">Tentar novamente</string>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">Ett nätverksfel uppstod. Kontrollera din anslutning och försök igen.</string>
<string name="error_network_fmt">Ett nätverksfel har uppstått: %s</string>
<string name="error_generic_fmt">Ett fel har uppstått: %s</string>
<string name="error_404_not_found_fmt">Din server stöder inte denna funktion: %1$s</string>
<string name="error_generic">Ett fel har uppstått.</string>
<string name="message_empty">Ingenting här.</string>
<string name="action_retry">Försök igen</string>

View File

@ -1,10 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="error_network">A network error occurred. Please check your connection and try again.</string>
<string name="error_network_fmt">A network error occurred: %s</string>
<string name="error_generic_fmt">An error occurred: %s</string>
<string name="error_404_not_found_fmt">Your server does not support this feature: %1$s</string>
<string name="error_json_data_fmt">Your server returned an invalid response: %1$s</string>
<string name="error_generic">An error occurred.</string>
<string name="message_empty">Nothing here.</string>
<string name="action_retry">Retry</string>

View File

@ -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()

View File

@ -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<Lists, ApiError>) {
private fun bind(state: Result<Lists, ListsError>) {
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 ->

View File

@ -155,7 +155,7 @@ class ListsForAccountFragment : DialogFragment() {
binding.listsView.hide()
binding.messageView.apply {
show()
setup(it.throwable) {
setup(it) {
viewModel.refresh()
load()
}

View File

@ -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<Result<ListsWithMembership, FlowError>>(Ok(ListsWithMembership.Loading))
val listsWithMembership = _listsWithMembership.asStateFlow()
private val _errors = Channel<Error>()
private val _errors = Channel<HasListId>()
val errors = _errors.receiveAsFlow()
private val listsWithMembershipMap = mutableMapOf<String, ListWithMembership>()
@ -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
}
}

View File

@ -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<out String>,
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