feat: Show user's lists in the left-side navigation menu (#514)
Previously to view a list the user either had to add it to a tab, or tap through "Lists" in the navigation menu to their list of lists, and then tap the list they want. Fix that, and show all their lists in a dedicated section in the menu, with a new "Manage lists" entry that's functionality identical to the old "Lists" entry (i.e., it shows their lists and allows them to create, delete, and edit list settings). To do that: - Implement a proper `ListsRepository` as the single source of truth for list implementation throughout the app. Expose the current list of lists as a flow, with methods to perform operations on the lists. - Collect the `ListsRepository` flow in `MainActivity` and use that to populate the menu and update it whenever the user's lists change. - Rewrite the activities and fragments that manipulate lists to use `ListRepository`. - Always show error snackbars when list operations fail. In particular, the HTTP code and error are always shown. - Delete the custom `Either` implementation, it's no longer used. - Add types for modelling API responses and errors, `ApiResponse` and `ApiError` respectively. The response includes the headers as well as the body, so it can replace the use of `NetworkResult` and `Response`. The actual result of the operation is expected to be carried in a `com.github.michaelbull.result.Result` type. Implement a Retrofit call adapter for these types. Unit tests for these borrow heavily from https://github.com/connyduck/networkresult-calladapter Additional user-visible changes: - Add an accessible "Refresh" menu item to `ListsActivity`. - Adding a list to a tab has a dialog with a "Manage lists" option. Previously that would close the dialog when clicked, so the user had to re-open it on returning from list management. Now the dialog stays open. - The soft keyboard automatically opens when creating or editing a list.
This commit is contained in:
parent
a4dc3b85bd
commit
442f3bc80c
|
@ -135,7 +135,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ga/strings.xml"
|
||||
line="172"
|
||||
line="171"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -146,7 +146,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-bg/strings.xml"
|
||||
line="179"
|
||||
line="178"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -190,7 +190,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-eu/strings.xml"
|
||||
line="208"
|
||||
line="207"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -201,7 +201,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-sl/strings.xml"
|
||||
line="231"
|
||||
line="230"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -212,7 +212,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="253"
|
||||
line="252"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -223,7 +223,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="254"
|
||||
line="253"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -234,7 +234,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-bn-rIN/strings.xml"
|
||||
line="259"
|
||||
line="258"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -245,7 +245,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-hi/strings.xml"
|
||||
line="282"
|
||||
line="281"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -256,7 +256,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ga/strings.xml"
|
||||
line="286"
|
||||
line="285"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -267,7 +267,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="293"
|
||||
line="292"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -278,21 +278,21 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="296"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="MissingQuantity"
|
||||
message="For locale "cs" (Czech) the following quantity should also be defined: `many` (e.g. "10.0 dne")"
|
||||
errorLine1=" <plurals name="favs">"
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="297"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="MissingQuantity"
|
||||
message="For locale "cs" (Czech) the following quantity should also be defined: `many` (e.g. "10.0 dne")"
|
||||
errorLine1=" <plurals name="favs">"
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="298"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="MissingQuantity"
|
||||
message="For locale "cs" (Czech) the following quantity should also be defined: `many` (e.g. "10.0 dne")"
|
||||
|
@ -300,7 +300,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="303"
|
||||
line="302"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -311,7 +311,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="323"
|
||||
line="322"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -322,7 +322,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="334"
|
||||
line="333"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -333,7 +333,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="348"
|
||||
line="347"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -344,7 +344,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="353"
|
||||
line="352"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -355,7 +355,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="358"
|
||||
line="357"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -366,7 +366,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="392"
|
||||
line="391"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -377,7 +377,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-bg/strings.xml"
|
||||
line="394"
|
||||
line="393"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -388,7 +388,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="420"
|
||||
line="419"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -399,7 +399,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="424"
|
||||
line="423"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -410,7 +410,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="428"
|
||||
line="427"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -421,7 +421,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="432"
|
||||
line="431"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -432,7 +432,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="440"
|
||||
line="439"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -443,7 +443,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="445"
|
||||
line="444"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -454,7 +454,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-ca/strings.xml"
|
||||
line="449"
|
||||
line="448"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -465,7 +465,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-cs/strings.xml"
|
||||
line="450"
|
||||
line="449"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -652,7 +652,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-nb-rNO/strings.xml"
|
||||
line="288"
|
||||
line="287"
|
||||
column="43"/>
|
||||
</issue>
|
||||
|
||||
|
@ -663,7 +663,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-nb-rNO/strings.xml"
|
||||
line="419"
|
||||
line="418"
|
||||
column="86"/>
|
||||
</issue>
|
||||
|
||||
|
@ -674,7 +674,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-tr/strings.xml"
|
||||
line="533"
|
||||
line="532"
|
||||
column="294"/>
|
||||
</issue>
|
||||
|
||||
|
@ -685,7 +685,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/values-nb-rNO/strings.xml"
|
||||
line="555"
|
||||
line="554"
|
||||
column="51"/>
|
||||
</issue>
|
||||
|
||||
|
@ -707,7 +707,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values-fa/strings.xml"
|
||||
line="206"
|
||||
line="205"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -751,7 +751,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="471"
|
||||
line="473"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -762,7 +762,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="654"
|
||||
line="656"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1653,7 +1653,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="434"
|
||||
line="436"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1664,7 +1664,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="437"
|
||||
line="439"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1675,7 +1675,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="438"
|
||||
line="440"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1686,7 +1686,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="439"
|
||||
line="441"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1697,7 +1697,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="440"
|
||||
line="442"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1708,7 +1708,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="441"
|
||||
line="443"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1719,7 +1719,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="442"
|
||||
line="444"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1730,7 +1730,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="443"
|
||||
line="445"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1741,7 +1741,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="444"
|
||||
line="446"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1752,7 +1752,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="445"
|
||||
line="447"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1763,7 +1763,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="509"
|
||||
line="511"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1774,7 +1774,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="510"
|
||||
line="512"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1785,7 +1785,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="523"
|
||||
line="525"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1796,7 +1796,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="524"
|
||||
line="526"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1807,7 +1807,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="525"
|
||||
line="527"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1818,7 +1818,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="527"
|
||||
line="529"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1829,7 +1829,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="567"
|
||||
line="569"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1840,7 +1840,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="609"
|
||||
line="611"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1851,7 +1851,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="615"
|
||||
line="617"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1862,7 +1862,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="645"
|
||||
line="647"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1873,7 +1873,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="673"
|
||||
line="675"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1906,7 +1906,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values-ja/strings.xml"
|
||||
line="254"
|
||||
line="253"
|
||||
column="37"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1917,7 +1917,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values-sl/strings.xml"
|
||||
line="257"
|
||||
line="256"
|
||||
column="37"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1928,7 +1928,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values-ga/strings.xml"
|
||||
line="310"
|
||||
line="309"
|
||||
column="37"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1961,7 +1961,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values-ko/strings.xml"
|
||||
line="337"
|
||||
line="336"
|
||||
column="54"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2987,12 +2987,12 @@
|
|||
<issue
|
||||
id="ReportShortcutUsage"
|
||||
message="Calling this method indicates use of dynamic shortcuts, but there are no calls to methods that track shortcut usage, such as `pushDynamicShortcut` or `reportShortcutUsed`. Calling these methods is recommended, as they track shortcut usage and allow launchers to adjust which shortcuts appear based on activation history. Please see https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#track-usage"
|
||||
errorLine1=" ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
errorLine1=" ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/ShareShortcutHelper.kt"
|
||||
line="85"
|
||||
column="9"/>
|
||||
line="84"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
|
|
|
@ -25,7 +25,10 @@ import android.widget.LinearLayout
|
|||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
@ -34,30 +37,51 @@ import app.pachli.core.activity.loadAvatar
|
|||
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.extensions.visible
|
||||
import app.pachli.core.data.repository.ListsError
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.network.model.TimelineAccount
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiError
|
||||
import app.pachli.core.preferences.PrefKeys
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import app.pachli.databinding.FragmentAccountsInListBinding
|
||||
import app.pachli.databinding.ItemFollowRequestBinding
|
||||
import app.pachli.util.BindingHolder
|
||||
import app.pachli.util.Either
|
||||
import app.pachli.util.unsafeLazy
|
||||
import app.pachli.viewmodel.Accounts
|
||||
import app.pachli.viewmodel.AccountsInListViewModel
|
||||
import app.pachli.viewmodel.State
|
||||
import app.pachli.viewmodel.SearchResults
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.get
|
||||
import com.github.michaelbull.result.mapBoth
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||
|
||||
/**
|
||||
* Display the members of a given list with UI to add/remove existing accounts
|
||||
* and search for followers to add them to the list.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class AccountsInListFragment : DialogFragment() {
|
||||
|
||||
private val viewModel: AccountsInListViewModel by viewModels()
|
||||
private val viewModel: AccountsInListViewModel by viewModels(
|
||||
extrasProducer = {
|
||||
defaultViewModelCreationExtras.withCreationCallback<AccountsInListViewModel.Factory> { factory ->
|
||||
factory.create(requireArguments().getString(ARG_LIST_ID)!!)
|
||||
}
|
||||
},
|
||||
)
|
||||
private val binding by viewBinding(FragmentAccountsInListBinding::bind)
|
||||
|
||||
private lateinit var listId: String
|
||||
private lateinit var listName: String
|
||||
private val adapter = Adapter()
|
||||
private val searchAdapter = SearchAdapter()
|
||||
|
@ -74,10 +98,7 @@ class AccountsInListFragment : DialogFragment() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, DR.style.AppDialogFragmentStyle)
|
||||
val args = requireArguments()
|
||||
listId = args.getString(LIST_ID_ARG)!!
|
||||
listName = args.getString(LIST_NAME_ARG)!!
|
||||
|
||||
viewModel.load(listId)
|
||||
listName = args.getString(ARG_LIST_NAME)!!
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -98,17 +119,34 @@ class AccountsInListFragment : DialogFragment() {
|
|||
|
||||
binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.accountsSearchRecycler.adapter = searchAdapter
|
||||
(binding.accountsSearchRecycler.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.state.collect { state ->
|
||||
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
|
||||
|
||||
when (state.accounts) {
|
||||
is Either.Right -> binding.messageView.hide()
|
||||
is Either.Left -> handleError(state.accounts.value)
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch { viewModel.accountsInList.collect(::bindAccounts) }
|
||||
launch {
|
||||
// Search results update whenever:
|
||||
// a. There is a set of accounts in this list
|
||||
// b. There is a new search result
|
||||
viewModel.accountsInList
|
||||
// Ignore updates where accounts are not loaded
|
||||
.filter { it.mapBoth({ it is Accounts.Loaded }, { false }) }
|
||||
.combine(viewModel.searchResults) { accountsInList, searchResults ->
|
||||
Pair(
|
||||
(accountsInList.get() as? Accounts.Loaded)?.accounts.orEmpty().toSet(),
|
||||
searchResults,
|
||||
)
|
||||
}
|
||||
.collectLatest { (accounts, searchResults) ->
|
||||
bindSearchResults(accounts, searchResults)
|
||||
}
|
||||
}
|
||||
|
||||
setupSearchView(state)
|
||||
launch {
|
||||
viewModel.errors.collect {
|
||||
handleError(it.throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,19 +169,37 @@ class AccountsInListFragment : DialogFragment() {
|
|||
)
|
||||
}
|
||||
|
||||
private fun setupSearchView(state: State) {
|
||||
if (state.searchResult == null) {
|
||||
searchAdapter.submitList(listOf())
|
||||
binding.accountsSearchRecycler.hide()
|
||||
binding.accountsRecycler.show()
|
||||
} else {
|
||||
val listAccounts = state.accounts.asRightOrNull() ?: listOf()
|
||||
val newList = state.searchResult.map { acc ->
|
||||
acc to listAccounts.contains(acc)
|
||||
private fun bindAccounts(accounts: Result<Accounts, ListsError>) {
|
||||
accounts.onSuccess {
|
||||
binding.messageView.hide()
|
||||
if (it is Accounts.Loaded) adapter.submitList(it.accounts)
|
||||
}.onFailure {
|
||||
binding.messageView.show()
|
||||
handleError(it.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindSearchResults(accountsInList: Set<TimelineAccount>, searchResults: Result<SearchResults, ApiError>) {
|
||||
searchResults.onSuccess { searchState ->
|
||||
when (searchState) {
|
||||
SearchResults.Empty -> {
|
||||
searchAdapter.submitList(emptyList())
|
||||
binding.accountsSearchRecycler.hide()
|
||||
binding.accountsRecycler.show()
|
||||
}
|
||||
SearchResults.Loading -> { /* nothing */ }
|
||||
is SearchResults.Loaded -> {
|
||||
val newList = searchState.accounts.map { account ->
|
||||
account to accountsInList.contains(account)
|
||||
}
|
||||
searchAdapter.submitList(newList)
|
||||
binding.accountsSearchRecycler.show()
|
||||
binding.accountsRecycler.hide()
|
||||
}
|
||||
}
|
||||
searchAdapter.submitList(newList)
|
||||
binding.accountsSearchRecycler.show()
|
||||
binding.accountsRecycler.hide()
|
||||
}.onFailure {
|
||||
Timber.w(it.throwable, "Error searching for accounts in list")
|
||||
handleError(it.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,17 +207,13 @@ class AccountsInListFragment : DialogFragment() {
|
|||
binding.messageView.show()
|
||||
binding.messageView.setup(error) { _: View ->
|
||||
binding.messageView.hide()
|
||||
viewModel.load(listId)
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRemoveFromList(accountId: String) {
|
||||
viewModel.deleteAccountFromList(listId, accountId)
|
||||
}
|
||||
private fun onRemoveFromList(accountId: String) = viewModel.deleteAccountFromList(accountId)
|
||||
|
||||
private fun onAddToList(account: TimelineAccount) {
|
||||
viewModel.addAccountToList(listId, account)
|
||||
}
|
||||
private fun onAddToList(account: TimelineAccount) = viewModel.addAccountToList(account)
|
||||
|
||||
private object AccountDiffer : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
|
@ -176,7 +228,6 @@ class AccountsInListFragment : DialogFragment() {
|
|||
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(
|
||||
AccountDiffer,
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val holder = BindingHolder(binding)
|
||||
|
@ -196,6 +247,7 @@ class AccountsInListFragment : DialogFragment() {
|
|||
val account = getItem(position)
|
||||
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
||||
holder.binding.usernameTextView.text = account.username
|
||||
holder.binding.avatarBadge.visible(account.bot)
|
||||
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
|
||||
}
|
||||
}
|
||||
|
@ -213,7 +265,6 @@ class AccountsInListFragment : DialogFragment() {
|
|||
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(
|
||||
SearchDiffer,
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val holder = BindingHolder(binding)
|
||||
|
@ -238,6 +289,7 @@ class AccountsInListFragment : DialogFragment() {
|
|||
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
||||
holder.binding.usernameTextView.text = account.username
|
||||
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
|
||||
holder.binding.avatarBadge.visible(account.bot)
|
||||
|
||||
holder.binding.rejectButton.apply {
|
||||
contentDescription = if (inAList) {
|
||||
|
@ -252,14 +304,14 @@ class AccountsInListFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val LIST_ID_ARG = "listId"
|
||||
private const val LIST_NAME_ARG = "listName"
|
||||
private const val ARG_LIST_ID = "listId"
|
||||
private const val ARG_LIST_NAME = "listName"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(listId: String, listName: String): AccountsInListFragment {
|
||||
val args = Bundle().apply {
|
||||
putString(LIST_ID_ARG, listId)
|
||||
putString(LIST_NAME_ARG, listName)
|
||||
putString(ARG_LIST_ID, listId)
|
||||
putString(ARG_LIST_NAME, listName)
|
||||
}
|
||||
return AccountsInListFragment().apply { arguments = args }
|
||||
}
|
||||
|
|
|
@ -20,14 +20,18 @@ package app.pachli
|
|||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageButton
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
@ -38,18 +42,19 @@ import app.pachli.core.activity.BaseActivity
|
|||
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.extensions.visible
|
||||
import app.pachli.core.data.repository.Lists
|
||||
import app.pachli.core.navigation.StatusListActivityIntent
|
||||
import app.pachli.core.network.model.MastoList
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiError
|
||||
import app.pachli.core.network.retrofit.apiresult.NetworkError
|
||||
import app.pachli.core.ui.await
|
||||
import app.pachli.databinding.ActivityListsBinding
|
||||
import app.pachli.databinding.DialogListBinding
|
||||
import app.pachli.viewmodel.Error
|
||||
import app.pachli.viewmodel.ListsViewModel
|
||||
import app.pachli.viewmodel.ListsViewModel.Event
|
||||
import app.pachli.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK
|
||||
import app.pachli.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
|
||||
import app.pachli.viewmodel.ListsViewModel.LoadingState.INITIAL
|
||||
import app.pachli.viewmodel.ListsViewModel.LoadingState.LOADED
|
||||
import app.pachli.viewmodel.ListsViewModel.LoadingState.LOADING
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -58,10 +63,14 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
|||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Shows all the user's lists, with UI to perform CRUD operations on the lists.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ListsActivity : BaseActivity() {
|
||||
class ListsActivity : BaseActivity(), MenuProvider {
|
||||
private val viewModel: ListsViewModel by viewModels()
|
||||
|
||||
private val binding by viewBinding(ActivityListsBinding::inflate)
|
||||
|
@ -86,106 +95,148 @@ class ListsActivity : BaseActivity() {
|
|||
MaterialDividerItemDecoration(this, MaterialDividerItemDecoration.VERTICAL),
|
||||
)
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener { viewModel.retryLoading() }
|
||||
binding.swipeRefreshLayout.setOnRefreshListener { viewModel.refresh() }
|
||||
binding.swipeRefreshLayout.setColorSchemeColors(MaterialColors.getColor(binding.root, androidx.appcompat.R.attr.colorPrimary))
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.state.collect(this@ListsActivity::update)
|
||||
}
|
||||
|
||||
viewModel.retryLoading()
|
||||
|
||||
binding.addListButton.setOnClickListener {
|
||||
showlistNameDialog(null)
|
||||
lifecycleScope.launch { showListNameDialog(null) }
|
||||
}
|
||||
|
||||
addMenuProvider(this)
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.lists.collectLatest(::bind)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.events.collect { event ->
|
||||
when (event) {
|
||||
Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||
Event.UPDATE_ERROR -> showMessage(R.string.error_rename_list)
|
||||
Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||
viewModel.errors.collect { error ->
|
||||
when (error) {
|
||||
is Error.Create -> showMessage(getString(R.string.error_create_list_fmt, error.title, error.throwable.message))
|
||||
is Error.Delete -> showMessage(getString(R.string.error_delete_list_fmt, error.title, error.throwable.message))
|
||||
is Error.Update -> showMessage(getString(R.string.error_rename_list_fmt, error.title, error.throwable.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showlistNameDialog(list: MastoList?) {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
super.onCreateMenu(menu, menuInflater)
|
||||
menuInflater.inflate(R.menu.activity_lists, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
super.onMenuItemSelected(menuItem)
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_refresh -> {
|
||||
refreshContent()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshContent() {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
private suspend fun showListNameDialog(list: MastoList?) {
|
||||
val binding = DialogListBinding.inflate(layoutInflater)
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(
|
||||
if (list == null) {
|
||||
R.string.action_create_list
|
||||
} else {
|
||||
R.string.action_rename_list
|
||||
},
|
||||
) { _, _ ->
|
||||
onPickedDialogName(binding.nameText.text.toString(), list?.id, binding.exclusiveCheckbox.isChecked)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.create()
|
||||
|
||||
binding.nameText.let { editText ->
|
||||
editText.doOnTextChanged { s, _, _, _ ->
|
||||
dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled = s?.isNotBlank() == true
|
||||
// Ensure the soft keyboard opens when the name field has focus
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
|
||||
|
||||
dialog.setOnShowListener {
|
||||
binding.nameText.let { editText ->
|
||||
editText.doOnTextChanged { s, _, _, _ ->
|
||||
dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled = s?.isNotBlank() == true
|
||||
}
|
||||
editText.setText(list?.title)
|
||||
editText.text?.let { editText.setSelection(it.length) }
|
||||
}
|
||||
editText.setText(list?.title)
|
||||
editText.text?.let { editText.setSelection(it.length) }
|
||||
|
||||
list?.let { list ->
|
||||
list.exclusive?.let {
|
||||
binding.exclusiveCheckbox.isChecked = it
|
||||
} ?: binding.exclusiveCheckbox.hide()
|
||||
}
|
||||
|
||||
binding.nameText.requestFocus()
|
||||
}
|
||||
|
||||
list?.exclusive?.let {
|
||||
binding.exclusiveCheckbox.isChecked = isTaskRoot
|
||||
} ?: binding.exclusiveCheckbox.hide()
|
||||
val result = dialog.await(
|
||||
list?.let { R.string.action_rename_list } ?: R.string.action_create_list,
|
||||
android.R.string.cancel,
|
||||
)
|
||||
|
||||
if (result == AlertDialog.BUTTON_POSITIVE) {
|
||||
onPickedDialogName(
|
||||
binding.nameText.text.toString(),
|
||||
list?.id,
|
||||
binding.exclusiveCheckbox.isChecked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showListDeleteDialog(list: MastoList) {
|
||||
AlertDialog.Builder(this)
|
||||
private suspend fun showListDeleteDialog(list: MastoList) {
|
||||
val result = AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.dialog_delete_list_warning, list.title))
|
||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||
viewModel.deleteList(list.id)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.create()
|
||||
.await(R.string.action_delete, android.R.string.cancel)
|
||||
|
||||
if (result == AlertDialog.BUTTON_POSITIVE) viewModel.deleteList(list.id, list.title)
|
||||
}
|
||||
|
||||
private fun update(state: ListsViewModel.State) {
|
||||
adapter.submitList(state.lists)
|
||||
binding.progressBar.visible(state.loadingState == LOADING)
|
||||
binding.swipeRefreshLayout.isRefreshing = state.loadingState == LOADING
|
||||
when (state.loadingState) {
|
||||
INITIAL, LOADING -> binding.messageView.hide()
|
||||
ERROR_NETWORK -> {
|
||||
binding.messageView.show()
|
||||
private fun bind(state: Result<Lists, ApiError>) {
|
||||
state.onFailure {
|
||||
binding.messageView.show()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
|
||||
if (it is NetworkError) {
|
||||
binding.messageView.setup(R.drawable.errorphant_offline, R.string.error_network) {
|
||||
viewModel.retryLoading()
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
ERROR_OTHER -> {
|
||||
binding.messageView.show()
|
||||
} else {
|
||||
binding.messageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
|
||||
viewModel.retryLoading()
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
LOADED ->
|
||||
if (state.lists.isEmpty()) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.message_empty,
|
||||
null,
|
||||
)
|
||||
} else {
|
||||
binding.messageView.hide()
|
||||
}
|
||||
|
||||
state.onSuccess { lists ->
|
||||
when (lists) {
|
||||
is Lists.Loaded -> {
|
||||
adapter.submitList(lists.lists.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title }))
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
if (lists.lists.isEmpty()) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.message_empty,
|
||||
null,
|
||||
)
|
||||
} else {
|
||||
binding.messageView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Lists.Loading -> {
|
||||
binding.messageView.hide()
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMessage(@StringRes messageId: Int) {
|
||||
private fun showMessage(message: String) {
|
||||
Snackbar.make(
|
||||
binding.listsRecycler,
|
||||
messageId,
|
||||
Snackbar.LENGTH_SHORT,
|
||||
binding.root,
|
||||
message,
|
||||
Snackbar.LENGTH_INDEFINITE,
|
||||
).show()
|
||||
}
|
||||
|
||||
|
@ -199,18 +250,14 @@ class ListsActivity : BaseActivity() {
|
|||
AccountsInListFragment.newInstance(list.id, list.title).show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
private fun renameListDialog(list: MastoList) {
|
||||
showlistNameDialog(list)
|
||||
}
|
||||
|
||||
private fun onMore(list: MastoList, view: View) {
|
||||
PopupMenu(view.context, view).apply {
|
||||
inflate(R.menu.list_actions)
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.list_edit -> openListSettings(list)
|
||||
R.id.list_update -> renameListDialog(list)
|
||||
R.id.list_delete -> showListDeleteDialog(list)
|
||||
R.id.list_update -> lifecycleScope.launch { showListNameDialog(list) }
|
||||
R.id.list_delete -> lifecycleScope.launch { showListDeleteDialog(list) }
|
||||
else -> return@setOnMenuItemClickListener false
|
||||
}
|
||||
true
|
||||
|
|
|
@ -74,6 +74,8 @@ import app.pachli.core.common.di.ApplicationScope
|
|||
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.data.repository.Lists
|
||||
import app.pachli.core.data.repository.ListsRepository
|
||||
import app.pachli.core.database.model.AccountEntity
|
||||
import app.pachli.core.database.model.TabKind
|
||||
import app.pachli.core.designsystem.EmbeddedFontFamily
|
||||
|
@ -121,7 +123,11 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
|||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.target.FixedSizeDrawable
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
@ -139,6 +145,7 @@ import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
|||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.SectionDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.Typefaceable
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||
|
@ -181,6 +188,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
@Inject
|
||||
lateinit var updateCheck: UpdateCheck
|
||||
|
||||
@Inject lateinit var listsRepository: ListsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var developerToolsUseCase: DeveloperToolsUseCase
|
||||
|
||||
|
@ -352,6 +361,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
listsRepository.lists.collect {
|
||||
it.onSuccess { refreshMainDrawerItems(addSearchButton = hideTopToolbar) }
|
||||
|
||||
it.onFailure {
|
||||
Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.action_retry) { listsRepository.refresh() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
externalScope.launch {
|
||||
// Flush old media that was cached for sharing
|
||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Pachli"))
|
||||
|
@ -571,6 +592,28 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
}
|
||||
|
||||
private fun refreshMainDrawerItems(addSearchButton: Boolean) {
|
||||
val (listsDrawerItems, listsSectionTitle) = listsRepository.lists.value.getOrElse { null }?.let { result ->
|
||||
when (result) {
|
||||
Lists.Loading -> Pair(emptyList(), R.string.title_lists_loading)
|
||||
is Lists.Loaded -> Pair(
|
||||
result.lists
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title })
|
||||
.map { list ->
|
||||
primaryDrawerItem {
|
||||
nameText = list.title
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(
|
||||
StatusListActivityIntent.list(this@MainActivity, list.id, list.title),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
R.string.title_lists,
|
||||
)
|
||||
}
|
||||
} ?: Pair(emptyList(), R.string.title_lists_failed)
|
||||
|
||||
binding.mainDrawer.apply {
|
||||
itemAdapter.clear()
|
||||
tintStatusBar = true
|
||||
|
@ -609,13 +652,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
SectionDrawerItem().apply {
|
||||
nameRes = listsSectionTitle
|
||||
},
|
||||
*listsDrawerItems.toTypedArray(),
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_lists
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||
nameRes = R.string.manage_lists
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ListActivityIntent(context))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_drafts
|
||||
iconRes = R.drawable.ic_notebook
|
||||
|
|
|
@ -46,6 +46,8 @@ 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.extensions.visible
|
||||
import app.pachli.core.data.repository.Lists
|
||||
import app.pachli.core.data.repository.ListsRepository
|
||||
import app.pachli.core.database.model.TabData
|
||||
import app.pachli.core.database.model.TabKind
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
|
@ -55,8 +57,9 @@ import app.pachli.core.network.retrofit.MastodonApi
|
|||
import app.pachli.databinding.ActivityTabPreferenceBinding
|
||||
import app.pachli.util.getDimension
|
||||
import app.pachli.util.unsafeLazy
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
|
@ -80,6 +83,9 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
|||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
|
||||
@Inject
|
||||
lateinit var listsRepository: ListsRepository
|
||||
|
||||
private val binding by viewBinding(ActivityTabPreferenceBinding::inflate)
|
||||
|
||||
private lateinit var currentTabs: MutableList<TabViewData>
|
||||
|
@ -302,12 +308,9 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
|||
statusLayout.addView(progress)
|
||||
statusLayout.addView(noListsText)
|
||||
|
||||
val dialogBuilder = AlertDialog.Builder(this)
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.select_list_title)
|
||||
.setNeutralButton(R.string.select_list_manage) { _, _ ->
|
||||
val listIntent = ListActivityIntent(applicationContext)
|
||||
startActivity(listIntent)
|
||||
}
|
||||
.setNeutralButton(R.string.select_list_manage, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setView(statusLayout)
|
||||
.setAdapter(adapter) { _, position ->
|
||||
|
@ -318,28 +321,39 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
|||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
}
|
||||
}
|
||||
}.create()
|
||||
|
||||
val showProgressBarJob = getProgressBarJob(progress, 500)
|
||||
showProgressBarJob.start()
|
||||
|
||||
val dialog = dialogBuilder.show()
|
||||
// Set the "Manage lists" button listener after creating the dialog. This ensures
|
||||
// that clicking the button does not dismiss the dialog, so when the user returns
|
||||
// from managing the lists the dialog is still displayed.
|
||||
dialog.setOnShowListener {
|
||||
val button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
|
||||
button.setOnClickListener {
|
||||
startActivity(ListActivityIntent(applicationContext))
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
|
||||
lifecycleScope.launch {
|
||||
mastodonApi.getLists().fold(
|
||||
{ lists ->
|
||||
showProgressBarJob.cancel()
|
||||
adapter.addAll(lists)
|
||||
if (lists.isEmpty()) {
|
||||
noListsText.show()
|
||||
listsRepository.lists.collect { result ->
|
||||
result.onSuccess { lists ->
|
||||
if (lists is Lists.Loaded) {
|
||||
showProgressBarJob.cancel()
|
||||
adapter.clear()
|
||||
adapter.addAll(lists.lists)
|
||||
if (lists.lists.isEmpty()) noListsText.show()
|
||||
}
|
||||
},
|
||||
{ throwable ->
|
||||
}
|
||||
|
||||
result.onFailure {
|
||||
dialog.hide()
|
||||
Timber.w(throwable, "failed to load lists")
|
||||
Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_LONG).show()
|
||||
},
|
||||
)
|
||||
Timber.w(it.throwable, "failed to load lists")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import app.pachli.R
|
||||
import app.pachli.components.account.list.ListsForAccountViewModel.Error
|
||||
import app.pachli.components.account.list.ListsForAccountViewModel.FlowError
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
|
@ -36,15 +38,29 @@ import app.pachli.core.designsystem.R as DR
|
|||
import app.pachli.databinding.FragmentListsForAccountBinding
|
||||
import app.pachli.databinding.ItemAddOrRemoveFromListBinding
|
||||
import app.pachli.util.BindingHolder
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Shows all the user's lists with a button to allow them to add/remove the given
|
||||
* account from each list.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ListsForAccountFragment : DialogFragment() {
|
||||
private val viewModel: ListsForAccountViewModel by viewModels(
|
||||
extrasProducer = {
|
||||
defaultViewModelCreationExtras.withCreationCallback<ListsForAccountViewModel.Factory> { factory ->
|
||||
factory.create(requireArguments().getString(ARG_ACCOUNT_ID)!!)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val viewModel: ListsForAccountViewModel by viewModels()
|
||||
private val binding by viewBinding(FragmentListsForAccountBinding::bind)
|
||||
|
||||
private val adapter = Adapter()
|
||||
|
@ -52,8 +68,6 @@ class ListsForAccountFragment : DialogFragment() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, DR.style.AppDialogFragmentStyle)
|
||||
|
||||
viewModel.setup(requireArguments().getString(ARG_ACCOUNT_ID)!!)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -79,45 +93,23 @@ class ListsForAccountFragment : DialogFragment() {
|
|||
binding.listsView.adapter = adapter
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.states.collectLatest { states ->
|
||||
binding.progressBar.hide()
|
||||
if (states.isEmpty()) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) {
|
||||
load()
|
||||
}
|
||||
} else {
|
||||
binding.listsView.show()
|
||||
adapter.submitList(states)
|
||||
}
|
||||
}
|
||||
viewModel.listsWithMembership.collectLatest(::bind)
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.loadError.collectLatest { error ->
|
||||
binding.progressBar.hide()
|
||||
binding.listsView.hide()
|
||||
binding.messageView.apply {
|
||||
show()
|
||||
setup(error) { load() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.actionError.collectLatest { error ->
|
||||
when (error.type) {
|
||||
ActionError.Type.ADD -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG)
|
||||
viewModel.errors.collectLatest { error ->
|
||||
when (error) {
|
||||
is Error.AddAccounts -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.addAccountToList(error.listId)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
ActionError.Type.REMOVE -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG)
|
||||
is Error.DeleteAccounts -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.removeAccountFromList(error.listId)
|
||||
viewModel.deleteAccountFromList(error.listId)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
@ -136,27 +128,60 @@ class ListsForAccountFragment : DialogFragment() {
|
|||
binding.progressBar.show()
|
||||
binding.listsView.hide()
|
||||
binding.messageView.hide()
|
||||
viewModel.load()
|
||||
}
|
||||
|
||||
private object Differ : DiffUtil.ItemCallback<AccountListState>() {
|
||||
private fun bind(result: Result<ListsWithMembership, FlowError>) {
|
||||
result.onSuccess {
|
||||
when (it) {
|
||||
ListsWithMembership.Loading -> {
|
||||
binding.progressBar.show()
|
||||
}
|
||||
is ListsWithMembership.Loaded -> {
|
||||
binding.progressBar.hide()
|
||||
if (it.listsWithMembership.isEmpty()) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) {
|
||||
load()
|
||||
}
|
||||
} else {
|
||||
binding.listsView.show()
|
||||
adapter.submitList(it.listsWithMembership.values.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.onFailure {
|
||||
binding.progressBar.hide()
|
||||
binding.listsView.hide()
|
||||
binding.messageView.apply {
|
||||
show()
|
||||
setup(it.throwable) {
|
||||
viewModel.refresh()
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object Differ : DiffUtil.ItemCallback<ListWithMembership>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: AccountListState,
|
||||
newItem: AccountListState,
|
||||
oldItem: ListWithMembership,
|
||||
newItem: ListWithMembership,
|
||||
): Boolean {
|
||||
return oldItem.list.id == newItem.list.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: AccountListState,
|
||||
newItem: AccountListState,
|
||||
oldItem: ListWithMembership,
|
||||
newItem: ListWithMembership,
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
inner class Adapter :
|
||||
ListAdapter<AccountListState, BindingHolder<ItemAddOrRemoveFromListBinding>>(Differ) {
|
||||
ListAdapter<ListWithMembership, BindingHolder<ItemAddOrRemoveFromListBinding>>(Differ) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
|
@ -170,21 +195,22 @@ class ListsForAccountFragment : DialogFragment() {
|
|||
val item = getItem(position)
|
||||
holder.binding.listNameView.text = item.list.title
|
||||
holder.binding.addButton.apply {
|
||||
visible(!item.includesAccount)
|
||||
visible(!item.isMember)
|
||||
setOnClickListener {
|
||||
viewModel.addAccountToList(item.list.id)
|
||||
}
|
||||
}
|
||||
holder.binding.removeButton.apply {
|
||||
visible(item.includesAccount)
|
||||
visible(item.isMember)
|
||||
setOnClickListener {
|
||||
viewModel.removeAccountFromList(item.list.id)
|
||||
viewModel.deleteAccountFromList(item.list.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** The ID of the account to add/remove the lists */
|
||||
private const val ARG_ACCOUNT_ID = "accountId"
|
||||
|
||||
fun newInstance(accountId: String): ListsForAccountFragment {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* Copyright 2022 kyori19
|
||||
/*
|
||||
* Copyright 2024 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
|
@ -18,122 +19,166 @@ package app.pachli.components.account.list
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.core.data.repository.HasListId
|
||||
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.MastodonApi
|
||||
import at.connyduck.calladapter.networkresult.getOrThrow
|
||||
import at.connyduck.calladapter.networkresult.onFailure
|
||||
import at.connyduck.calladapter.networkresult.onSuccess
|
||||
import at.connyduck.calladapter.networkresult.runCatching
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiError
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlin.collections.set
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.toImmutableMap
|
||||
|
||||
data class AccountListState(
|
||||
sealed interface ListsWithMembership {
|
||||
data object Loading : ListsWithMembership
|
||||
data class Loaded(val listsWithMembership: Map<String, ListWithMembership>) : ListsWithMembership
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MastoList] with a property for whether [ListsForAccountViewModel.accountId] is a
|
||||
* member of the list.
|
||||
*
|
||||
* @property list The Mastodon list
|
||||
* @property isMember True if this list contains [ListsForAccountViewModel.accountId]
|
||||
*/
|
||||
data class ListWithMembership(
|
||||
val list: MastoList,
|
||||
val includesAccount: Boolean,
|
||||
val isMember: Boolean,
|
||||
)
|
||||
|
||||
data class ActionError(
|
||||
val error: Throwable,
|
||||
val type: Type,
|
||||
val listId: String,
|
||||
) : Throwable(error) {
|
||||
enum class Type {
|
||||
ADD,
|
||||
REMOVE,
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
class ListsForAccountViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
@HiltViewModel(assistedFactory = ListsForAccountViewModel.Factory::class)
|
||||
class ListsForAccountViewModel @AssistedInject constructor(
|
||||
private val listsRepository: ListsRepository,
|
||||
@Assisted val accountId: String,
|
||||
) : ViewModel() {
|
||||
private val _listsWithMembership = MutableStateFlow<Result<ListsWithMembership, FlowError>>(Ok(ListsWithMembership.Loading))
|
||||
val listsWithMembership = _listsWithMembership.asStateFlow()
|
||||
|
||||
private lateinit var accountId: String
|
||||
private val _errors = Channel<Error>()
|
||||
val errors = _errors.receiveAsFlow()
|
||||
|
||||
private val _states = MutableSharedFlow<List<AccountListState>>(1)
|
||||
val states: SharedFlow<List<AccountListState>> = _states
|
||||
private val listsWithMembershipMap = mutableMapOf<String, ListWithMembership>()
|
||||
|
||||
private val _loadError = MutableSharedFlow<Throwable>(1)
|
||||
val loadError: SharedFlow<Throwable> = _loadError
|
||||
|
||||
private val _actionError = MutableSharedFlow<ActionError>(1)
|
||||
val actionError: SharedFlow<ActionError> = _actionError
|
||||
|
||||
fun setup(accountId: String) {
|
||||
this.accountId = accountId
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun load() {
|
||||
_loadError.resetReplayCache()
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
val (all, includes) = listOf(
|
||||
async { mastodonApi.getLists() },
|
||||
async { mastodonApi.getListsIncludesAccount(accountId) },
|
||||
).awaitAll()
|
||||
|
||||
_states.emit(
|
||||
all.getOrThrow().map { list ->
|
||||
AccountListState(
|
||||
list = list,
|
||||
includesAccount = includes.getOrThrow().any { it.id == list.id },
|
||||
)
|
||||
},
|
||||
)
|
||||
/**
|
||||
* Takes the user's lists, and the subset of those lists that [accountId] is a member of,
|
||||
* and merges them to produce a map of [ListWithMembership].
|
||||
*/
|
||||
fun refresh() = viewModelScope.launch {
|
||||
_listsWithMembership.value = Ok(ListsWithMembership.Loading)
|
||||
listsRepository.lists.collect { result ->
|
||||
val lists = result.getOrElse {
|
||||
_listsWithMembership.value = Err(Error.Retrieve(it))
|
||||
return@collect
|
||||
}
|
||||
.onFailure {
|
||||
_loadError.emit(it)
|
||||
|
||||
if (lists !is Lists.Loaded) return@collect
|
||||
|
||||
_listsWithMembership.value = with(listsWithMembershipMap) {
|
||||
val memberLists = listsRepository.getListsWithAccount(accountId)
|
||||
.getOrElse { return@with Err(Error.GetListsWithAccount(it)) }
|
||||
|
||||
clear()
|
||||
|
||||
memberLists.forEach { list ->
|
||||
put(list.id, ListWithMembership(list, true))
|
||||
}
|
||||
|
||||
lists.lists.forEach { list ->
|
||||
putIfAbsent(list.id, ListWithMembership(list, false))
|
||||
}
|
||||
|
||||
Ok(ListsWithMembership.Loaded(listsWithMembershipMap.toImmutableMap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAccountToList(listId: String) {
|
||||
_actionError.resetReplayCache()
|
||||
viewModelScope.launch {
|
||||
mastodonApi.addAccountToList(listId, listOf(accountId))
|
||||
.onSuccess {
|
||||
_states.emit(
|
||||
_states.first().map { state ->
|
||||
if (state.list.id == listId) {
|
||||
state.copy(includesAccount = true)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
_actionError.emit(ActionError(it, ActionError.Type.ADD, listId))
|
||||
}
|
||||
/**
|
||||
* Fallibly adds [accountId] to [listId], sending [Error.AddAccounts] on failure.
|
||||
*/
|
||||
fun addAccountToList(listId: String) = viewModelScope.launch {
|
||||
// Optimistically update so the UI is snappy
|
||||
listsWithMembershipMap[listId]?.let {
|
||||
listsWithMembershipMap[listId] = it.copy(isMember = true)
|
||||
}
|
||||
|
||||
_listsWithMembership.value = Ok(ListsWithMembership.Loaded(listsWithMembershipMap.toImmutableMap()))
|
||||
|
||||
listsRepository.addAccountsToList(listId, listOf(accountId)).onFailure { error ->
|
||||
// Undo the optimistic update
|
||||
listsWithMembershipMap[listId]?.let {
|
||||
listsWithMembershipMap[listId] = it.copy(isMember = false)
|
||||
}
|
||||
|
||||
_listsWithMembership.value = Ok(ListsWithMembership.Loaded(listsWithMembershipMap.toImmutableMap()))
|
||||
|
||||
_errors.send(Error.AddAccounts(error))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAccountFromList(listId: String) {
|
||||
_actionError.resetReplayCache()
|
||||
viewModelScope.launch {
|
||||
mastodonApi.deleteAccountFromList(listId, listOf(accountId))
|
||||
.onSuccess {
|
||||
_states.emit(
|
||||
_states.first().map { state ->
|
||||
if (state.list.id == listId) {
|
||||
state.copy(includesAccount = false)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
_actionError.emit(ActionError(it, ActionError.Type.REMOVE, listId))
|
||||
}
|
||||
/**
|
||||
* Fallibly deletes [accountId] from [listId], sending [Error.DeleteAccounts] on failure.
|
||||
*/
|
||||
fun deleteAccountFromList(listId: String) = viewModelScope.launch {
|
||||
// Optimistically update so the UI is snappy
|
||||
listsWithMembershipMap[listId]?.let {
|
||||
listsWithMembershipMap[listId] = it.copy(isMember = false)
|
||||
}
|
||||
_listsWithMembership.value = Ok(ListsWithMembership.Loaded(listsWithMembershipMap.toImmutableMap()))
|
||||
|
||||
listsRepository.deleteAccountsFromList(listId, listOf(accountId)).onFailure { error ->
|
||||
// Undo the optimistic update
|
||||
listsWithMembershipMap[listId]?.let {
|
||||
listsWithMembershipMap[listId] = it.copy(isMember = true)
|
||||
}
|
||||
|
||||
_listsWithMembership.value = Ok(ListsWithMembership.Loaded(listsWithMembershipMap.toImmutableMap()))
|
||||
|
||||
_errors.send(Error.DeleteAccounts(error))
|
||||
}
|
||||
}
|
||||
|
||||
/** Create [ListsForAccountViewModel] injecting [accountId] */
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(accountId: String): ListsForAccountViewModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker for errors that can be part of the [Result] in the
|
||||
* [ListsForAccountViewModel.listsWithMembership] flow
|
||||
*/
|
||||
sealed interface FlowError : ApiError
|
||||
|
||||
/** 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
|
||||
|
||||
@JvmInline
|
||||
value class Retrieve(val error: ListsError.Retrieve) : FlowError, ListsError by error
|
||||
|
||||
@JvmInline
|
||||
value class AddAccounts(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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import app.pachli.service.MediaToSend
|
|||
import app.pachli.service.ServiceClient
|
||||
import app.pachli.service.StatusToSend
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import com.github.michaelbull.result.mapBoth
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -381,13 +382,13 @@ class ComposeViewModel @Inject constructor(
|
|||
suspend fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
||||
when (token[0]) {
|
||||
'@' -> {
|
||||
return api.searchAccounts(query = token.substring(1), limit = 10)
|
||||
.fold({ accounts ->
|
||||
accounts.map { AutocompleteResult.AccountResult(it) }
|
||||
}, { e ->
|
||||
Timber.e(e, "Autocomplete search for %s failed.", token)
|
||||
return api.searchAccounts(query = token.substring(1), limit = 10).mapBoth(
|
||||
{ it.body.map { AutocompleteResult.AccountResult(it) } },
|
||||
{
|
||||
Timber.e(it.throwable, "Autocomplete search for %s failed.", token)
|
||||
emptyList()
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
'#' -> {
|
||||
return api.search(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* 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.util
|
||||
|
||||
/**
|
||||
* Class to represent sum type/tagged union/variant/ADT e.t.c.
|
||||
* It is either Left or Right.
|
||||
*/
|
||||
sealed class Either<out L, out R> {
|
||||
data class Left<out L, out R>(val value: L) : Either<L, R>()
|
||||
data class Right<out L, out R>(val value: R) : Either<L, R>()
|
||||
|
||||
fun isRight() = this is Right
|
||||
|
||||
fun isLeft() = this is Left
|
||||
|
||||
fun asLeftOrNull() = (this as? Left<L, R>)?.value
|
||||
|
||||
fun asRightOrNull() = (this as? Right<L, R>)?.value
|
||||
|
||||
fun asLeft(): L = (this as Left<L, R>).value
|
||||
|
||||
fun asRight(): R = (this as Right<L, R>).value
|
||||
|
||||
inline fun <N> map(crossinline mapper: (R) -> N): Either<L, N> {
|
||||
return if (this.isLeft()) {
|
||||
Left(this.asLeft())
|
||||
} else {
|
||||
Right(mapper(this.asRight()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,6 +44,6 @@ fun Throwable.getDrawableRes(): Int = when (this) {
|
|||
/** @return A string error message for this throwable */
|
||||
fun Throwable.getErrorString(context: Context): String = getServerErrorMessage() ?: when (this) {
|
||||
is IOException -> context.getString(R.string.error_network_fmt, this.message)
|
||||
is HttpException -> if (this.code() == 404) context.getString(R.string.error_404_not_found) else context.getString(R.string.error_generic)
|
||||
is HttpException -> if (this.code() == 404) context.getString(R.string.error_404_not_found_fmt, this.message) else context.getString(R.string.error_generic_fmt, this.message)
|
||||
else -> context.getString(R.string.error_generic_fmt, this.message)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
/*
|
||||
* Copyright 2024 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
|
@ -18,103 +19,137 @@ package app.pachli.viewmodel
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.core.data.repository.ListsError
|
||||
import app.pachli.core.data.repository.ListsRepository
|
||||
import app.pachli.core.network.model.TimelineAccount
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.util.Either
|
||||
import app.pachli.util.Either.Left
|
||||
import app.pachli.util.Either.Right
|
||||
import app.pachli.util.withoutFirstWhich
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiError
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.mapBoth
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
data class State(val accounts: Either<Throwable, List<TimelineAccount>>, val searchResult: List<TimelineAccount>?)
|
||||
sealed interface SearchResults {
|
||||
/** Search not started */
|
||||
data object Empty : SearchResults
|
||||
|
||||
@HiltViewModel
|
||||
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
|
||||
/** Search results are loading */
|
||||
data object Loading : SearchResults
|
||||
|
||||
val state: Flow<State> get() = _state
|
||||
private val _state = MutableStateFlow(State(Right(listOf()), null))
|
||||
/** Search results are loaded, discovered [accounts] */
|
||||
data class Loaded(val accounts: List<TimelineAccount>) : SearchResults
|
||||
}
|
||||
|
||||
fun load(listId: String) {
|
||||
val state = _state.value
|
||||
if (state.accounts.isLeft() || state.accounts.asRight().isEmpty()) {
|
||||
viewModelScope.launch {
|
||||
api.getAccountsInList(listId, 0).fold(
|
||||
{ accounts ->
|
||||
updateState { copy(accounts = Right(accounts)) }
|
||||
},
|
||||
{ e ->
|
||||
updateState { copy(accounts = Left(e)) }
|
||||
},
|
||||
)
|
||||
sealed interface Accounts {
|
||||
data object Loading : Accounts
|
||||
data class Loaded(val accounts: List<TimelineAccount>) : Accounts
|
||||
}
|
||||
|
||||
@HiltViewModel(assistedFactory = AccountsInListViewModel.Factory::class)
|
||||
class AccountsInListViewModel @AssistedInject constructor(
|
||||
private val api: MastodonApi,
|
||||
private val listsRepository: ListsRepository,
|
||||
@Assisted val listId: String,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _accountsInList = MutableStateFlow<Result<Accounts, FlowError.GetAccounts>>(Ok(Accounts.Loading))
|
||||
val accountsInList = _accountsInList.asStateFlow()
|
||||
|
||||
private val _searchResults = MutableStateFlow<Result<SearchResults, ApiError>>(Ok(SearchResults.Empty))
|
||||
|
||||
/** Flow of results after calling [search] */
|
||||
val searchResults = _searchResults.asStateFlow()
|
||||
|
||||
private val _errors = Channel<Error>()
|
||||
val errors = _errors.receiveAsFlow()
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() = viewModelScope.launch {
|
||||
_accountsInList.value = Ok(Accounts.Loading)
|
||||
_accountsInList.value = listsRepository.getAccountsInList(listId)
|
||||
.mapBoth({
|
||||
Ok(Accounts.Loaded(it))
|
||||
}, {
|
||||
Err(FlowError.GetAccounts(it))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add [account] to [listId], refreshing on success, sending [Error.AddAccounts] on failure
|
||||
*/
|
||||
fun addAccountToList(account: TimelineAccount) = viewModelScope.launch {
|
||||
listsRepository.addAccountsToList(listId, listOf(account.id))
|
||||
.onSuccess { refresh() }
|
||||
.onFailure {
|
||||
Timber.e("Failed to add account to list: %s", account.username)
|
||||
_errors.send(Error.AddAccounts(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAccountToList(listId: String, account: TimelineAccount) {
|
||||
viewModelScope.launch {
|
||||
api.addAccountToList(listId, listOf(account.id))
|
||||
.fold(
|
||||
{
|
||||
updateState {
|
||||
copy(accounts = accounts.map { it + account })
|
||||
}
|
||||
},
|
||||
{
|
||||
Timber.i(
|
||||
"Failed to add account to list: ${account.username}",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAccountFromList(listId: String, accountId: String) {
|
||||
viewModelScope.launch {
|
||||
api.deleteAccountFromList(listId, listOf(accountId))
|
||||
.fold(
|
||||
{
|
||||
updateState {
|
||||
copy(
|
||||
accounts = accounts.map { accounts ->
|
||||
accounts.withoutFirstWhich { it.id == accountId }
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Timber.i(
|
||||
"Failed to remove account from list: $accountId",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Remove [accountId] from [listId], refreshing on success, sending
|
||||
* [Error.DeleteAccounts] on failure
|
||||
*/
|
||||
fun deleteAccountFromList(accountId: String) = viewModelScope.launch {
|
||||
listsRepository.deleteAccountsFromList(listId, listOf(accountId))
|
||||
.onSuccess { refresh() }
|
||||
.onFailure {
|
||||
Timber.e("Failed to remove account from list: %s", accountId)
|
||||
_errors.send(Error.DeleteAccounts(it))
|
||||
}
|
||||
}
|
||||
|
||||
/** Search for [query] and send results to [searchResults] */
|
||||
fun search(query: String) {
|
||||
when {
|
||||
query.isEmpty() -> updateState { copy(searchResult = null) }
|
||||
query.isBlank() -> updateState { copy(searchResult = listOf()) }
|
||||
query.isEmpty() -> _searchResults.value = Ok(SearchResults.Empty)
|
||||
query.isBlank() -> _searchResults.value = Ok(SearchResults.Loaded(emptyList()))
|
||||
else -> viewModelScope.launch {
|
||||
api.searchAccounts(query, null, 10, true)
|
||||
.fold(
|
||||
{ result ->
|
||||
updateState { copy(searchResult = result) }
|
||||
},
|
||||
{
|
||||
updateState { copy(searchResult = listOf()) }
|
||||
},
|
||||
)
|
||||
_searchResults.value = api.searchAccounts(query, null, 10, true).mapBoth({
|
||||
Ok(SearchResults.Loaded(it.body))
|
||||
}, {
|
||||
Err(it)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||
_state.value = fn(_state.value)
|
||||
/** Create [AccountsInListViewModel] injecting [listId] */
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(listId: String): AccountsInListViewModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors that can be part of the [Result] in the
|
||||
* [AccountsInListViewModel.accountsInList] flow
|
||||
*/
|
||||
sealed interface FlowError : ListsError {
|
||||
@JvmInline
|
||||
value class GetAccounts(private val error: ListsError.GetAccounts) : FlowError, ListsError by error
|
||||
}
|
||||
|
||||
/** Asynchronous errors from network operations */
|
||||
sealed interface Error : ListsError {
|
||||
@JvmInline
|
||||
value class AddAccounts(private val error: ListsError.AddAccounts) : Error, ListsError by error
|
||||
|
||||
@JvmInline
|
||||
value class DeleteAccounts(private val error: ListsError.DeleteAccounts) : Error, ListsError by error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
/*
|
||||
* Copyright 2024 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
|
@ -18,130 +19,57 @@ package app.pachli.viewmodel
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.core.network.model.MastoList
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.util.replacedFirstWhich
|
||||
import app.pachli.util.withoutFirstWhich
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import app.pachli.core.data.repository.ListsError
|
||||
import app.pachli.core.data.repository.ListsRepository
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.IOException
|
||||
import java.net.ConnectException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed interface Error : ListsError {
|
||||
val title: String
|
||||
|
||||
data class Create(override val title: String, private val error: ListsError.Create) : Error, ListsError by error
|
||||
|
||||
data class Delete(override val title: String, private val error: ListsError.Delete) : Error, ListsError by error
|
||||
|
||||
data class Update(override val title: String, private val error: ListsError.Update) : Error, ListsError by error
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
|
||||
enum class LoadingState {
|
||||
INITIAL,
|
||||
LOADING,
|
||||
LOADED,
|
||||
ERROR_NETWORK,
|
||||
ERROR_OTHER,
|
||||
internal class ListsViewModel @Inject constructor(
|
||||
private val listsRepository: ListsRepository,
|
||||
) : ViewModel() {
|
||||
private val _errors = Channel<Error>()
|
||||
val errors = _errors.receiveAsFlow()
|
||||
|
||||
val lists = listsRepository.lists
|
||||
|
||||
init {
|
||||
listsRepository.refresh()
|
||||
}
|
||||
|
||||
enum class Event {
|
||||
CREATE_ERROR,
|
||||
DELETE_ERROR,
|
||||
UPDATE_ERROR,
|
||||
fun refresh() = viewModelScope.launch {
|
||||
listsRepository.refresh()
|
||||
}
|
||||
|
||||
data class State(val lists: List<MastoList>, val loadingState: LoadingState)
|
||||
|
||||
val state: Flow<State> get() = _state
|
||||
val events: Flow<Event> get() = _events
|
||||
private val _state = MutableStateFlow(State(listOf(), LoadingState.INITIAL))
|
||||
private val _events = MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
fun retryLoading() {
|
||||
loadIfNeeded()
|
||||
}
|
||||
|
||||
private fun loadIfNeeded() {
|
||||
val state = _state.value
|
||||
if (state.loadingState == LoadingState.LOADING || state.lists.isNotEmpty()) return
|
||||
updateState {
|
||||
copy(loadingState = LoadingState.LOADING)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
api.getLists().fold(
|
||||
{ lists ->
|
||||
updateState {
|
||||
copy(
|
||||
lists = lists,
|
||||
loadingState = LoadingState.LOADED,
|
||||
)
|
||||
}
|
||||
},
|
||||
{ err ->
|
||||
updateState {
|
||||
copy(
|
||||
loadingState = if (err is IOException || err is ConnectException) {
|
||||
LoadingState.ERROR_NETWORK
|
||||
} else {
|
||||
LoadingState.ERROR_OTHER
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
fun createNewList(title: String, exclusive: Boolean) = viewModelScope.launch {
|
||||
listsRepository.createList(title, exclusive).onFailure {
|
||||
_errors.send(Error.Create(title, it))
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewList(listName: String, exclusive: Boolean) {
|
||||
viewModelScope.launch {
|
||||
api.createList(listName, exclusive).fold(
|
||||
{ list ->
|
||||
updateState {
|
||||
copy(lists = lists + list)
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.CREATE_ERROR)
|
||||
},
|
||||
)
|
||||
fun updateList(listId: String, title: String, exclusive: Boolean) = viewModelScope.launch {
|
||||
listsRepository.editList(listId, title, exclusive).onFailure {
|
||||
_errors.send(Error.Update(title, it))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateList(listId: String, listName: String, exclusive: Boolean) {
|
||||
viewModelScope.launch {
|
||||
api.updateList(listId, listName, exclusive).fold(
|
||||
{ list ->
|
||||
updateState {
|
||||
copy(lists = lists.replacedFirstWhich(list) { it.id == listId })
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.UPDATE_ERROR)
|
||||
},
|
||||
)
|
||||
fun deleteList(listId: String, title: String) = viewModelScope.launch {
|
||||
listsRepository.deleteList(listId).onFailure {
|
||||
_errors.send(Error.Delete(title, it))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteList(listId: String) {
|
||||
viewModelScope.launch {
|
||||
api.deleteList(listId).fold(
|
||||
{
|
||||
updateState {
|
||||
copy(lists = lists.withoutFirstWhich { it.id == listId })
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.DELETE_ERROR)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||
_state.value = fn(_state.value)
|
||||
}
|
||||
|
||||
private suspend fun sendEvent(event: Event) {
|
||||
_events.emit(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,17 +41,6 @@
|
|||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<app.pachli.view.BackgroundMessageView
|
||||
android:id="@+id/messageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@android:color/transparent"
|
||||
android:visibility="gone"
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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>.
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_refresh"
|
||||
android:title="@string/action_refresh"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -238,11 +238,10 @@
|
|||
<string name="filter_add_description">العبارة التي يلزم تصفيتها</string>
|
||||
<string name="add_account_name">إضافة حساب</string>
|
||||
<string name="add_account_description">إضافة حساب ماستدون جديد</string>
|
||||
<string name="action_lists">القوائم</string>
|
||||
<string name="title_lists">القوائم</string>
|
||||
<string name="error_create_list">لا يمكن إنشاء قائمة</string>
|
||||
<string name="error_rename_list">لا يمكن إعادة تسمية القائمة</string>
|
||||
<string name="error_delete_list">لا يمكن حذف القائمة</string>
|
||||
<string name="error_create_list_fmt">%2$s: \"%1$s\" لا يمكن إنشاء قائمة</string>
|
||||
<string name="error_rename_list_fmt">%2$s: \"%1$s\" لا يمكن إعادة تسمية القائمة</string>
|
||||
<string name="error_delete_list_fmt">%2$s: \"%1$s\" لا يمكن حذف القائمة</string>
|
||||
<string name="action_create_list">إنشاء قائمة</string>
|
||||
<string name="action_rename_list">إعادة تسمية القائمة</string>
|
||||
<string name="action_delete_list">حذف القائمة</string>
|
||||
|
@ -658,7 +657,7 @@
|
|||
<string name="filter_description_hide">إخفاء تماما</string>
|
||||
<string name="filter_description_format">%s: %s</string>
|
||||
<string name="post_media_image">صورة</string>
|
||||
<string name="error_404_not_found">خادمك لا يدعم هذه الميزة</string>
|
||||
<string name="error_404_not_found_fmt">خادمك لا يدعم هذه الميزة: %1$s</string>
|
||||
<string name="filter_action_hide">إخفاء</string>
|
||||
<string name="label_filter_title">العنوان</string>
|
||||
<string name="pref_title_font_family">عائلة الخطوط</string>
|
||||
|
|
|
@ -363,11 +363,10 @@
|
|||
<string name="profile_metadata_label">Метазвесткі профілю</string>
|
||||
<string name="title_favourited_by">Упадабалі</string>
|
||||
<string name="add_account_description">Дадаць уліковы запіс Mastodon</string>
|
||||
<string name="action_lists">Спісы</string>
|
||||
<string name="title_lists">Спісы</string>
|
||||
<string name="error_create_list">Не атрымалася стварыць спіс</string>
|
||||
<string name="error_rename_list">Не атрымалася перайменаваць спіс</string>
|
||||
<string name="error_delete_list">Не атрымалася выдаліць спіс</string>
|
||||
<string name="error_create_list_fmt">Не атрымалася стварыць спіс \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Не атрымалася перайменаваць спіс \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Не атрымалася выдаліць спіс \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Стварыць спіс</string>
|
||||
<string name="action_rename_list">Перайменаваць спіс</string>
|
||||
<string name="action_delete_list">Выдаліць спіс</string>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
<string name="action_view_profile">ⴰⵎⴻⵖⵏⵓ</string>
|
||||
<string name="error_generic">ⵝⴻⵍⵍⴰ ⴷ ⵝⵓⵛⴹⴰ.</string>
|
||||
<string name="title_lists">ⵝⴰⴲⴸⴰⵔⵉⵏ</string>
|
||||
<string name="action_lists">ⵝⵉⴲⴸⴰⵔⵉⵏ</string>
|
||||
<string name="action_reset_schedule">ⵡⴻⵏⵏⴻⵣ ⵝⵉⴽⴻⵍⵜ ⵏⵏⵉⴸⴻⵏ</string>
|
||||
<string name="action_search">ⵏⴰⴸⵉ</string>
|
||||
<string name="action_edit_profile">ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ</string>
|
||||
|
|
|
@ -136,11 +136,10 @@
|
|||
<string name="action_delete_list">Изтриване на списъка</string>
|
||||
<string name="action_rename_list">Преименуване на списъка</string>
|
||||
<string name="action_create_list">Създаване на списък</string>
|
||||
<string name="error_delete_list">Списъкът не можа да се изтрие</string>
|
||||
<string name="error_create_list">Списъкът не можа да се създаде</string>
|
||||
<string name="error_rename_list">Списъкът не можа да се преименува</string>
|
||||
<string name="error_delete_list_fmt">Списъкът не можа да се изтрие \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">Списъкът не можа да се създаде \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Списъкът не можа да се преименува \"%1$s\": %2$s</string>
|
||||
<string name="title_lists">Списъци</string>
|
||||
<string name="action_lists">Списъци</string>
|
||||
<string name="add_account_description">Добавяне на нов Mastodon акаунт</string>
|
||||
<string name="add_account_name">Добавяне на акаунт</string>
|
||||
<string name="filter_add_description">Фраза за филтриране</string>
|
||||
|
|
|
@ -60,11 +60,10 @@
|
|||
<string name="action_delete_list">তালিকা মুছে দিন</string>
|
||||
<string name="action_rename_list">তালিকা পুনঃ নামকরণ কর</string>
|
||||
<string name="action_create_list">একটি তালিকা তৈরি করুন</string>
|
||||
<string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string>
|
||||
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
||||
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
||||
<string name="error_delete_list_fmt">তালিকা মুছে ফেলা যায়নি \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">তালিকা নামকরণ করা যায়নি \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">তালিকা তৈরি করা যায়নি \"%1$s\": %2$s</string>
|
||||
<string name="title_lists">তালিকাসমূহ</string>
|
||||
<string name="action_lists">তালিকাসমূহ</string>
|
||||
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
||||
<string name="add_account_name">অ্যাকাউন্ট যোগ করুন</string>
|
||||
<string name="filter_add_description">বাক্য ফিল্টার কর</string>
|
||||
|
|
|
@ -244,11 +244,10 @@
|
|||
<string name="filter_add_description">বাক্য ফিল্টার কর</string>
|
||||
<string name="add_account_name">অ্যাকাউন্ট যোগ করুন</string>
|
||||
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
||||
<string name="action_lists">তালিকাসমূহ</string>
|
||||
<string name="title_lists">তালিকাসমূহ</string>
|
||||
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
||||
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
||||
<string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string>
|
||||
<string name="error_create_list_fmt">তালিকা তৈরি করা যায়নি \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">তালিকা নামকরণ করা যায়নি \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">তালিকা মুছে ফেলা যায়নি \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">একটি তালিকা তৈরি করুন</string>
|
||||
<string name="action_rename_list">তালিকা পুনঃ নামকরণ কর</string>
|
||||
<string name="action_delete_list">তালিকা মুছে দিন</string>
|
||||
|
|
|
@ -238,11 +238,10 @@
|
|||
<string name="filter_dialog_update_button">Actualització</string>
|
||||
<string name="filter_add_description">Frase per filtrar</string>
|
||||
<string name="add_account_description">Afegir un compte de Mastodont</string>
|
||||
<string name="action_lists">Llistes</string>
|
||||
<string name="title_lists">Llistes</string>
|
||||
<string name="error_create_list">És impossible crear la llista</string>
|
||||
<string name="error_rename_list">Impossible reanomenar la llista</string>
|
||||
<string name="error_delete_list">És impossible suprimir la llista</string>
|
||||
<string name="error_create_list_fmt">És impossible crear la llista \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Impossible reanomenar la llista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">És impossible suprimir la llista \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Crear una llista</string>
|
||||
<string name="action_rename_list">Reanomenar la llista</string>
|
||||
<string name="action_delete_list">Suprimir la llista</string>
|
||||
|
|
|
@ -356,18 +356,17 @@
|
|||
<item quantity="other">وەسف بکە بۆ بینایی داڕماو
|
||||
\n(%d سنوری کاراکتەر)</item>
|
||||
</plurals>
|
||||
<string name="compose_active_account_description">بڵاوکردنەوە بە هەژماری %1$s</string>
|
||||
<string name="compose_active_account_description">%1$s بڵاوکردنەوە بە هەژماری</string>
|
||||
<string name="action_remove_from_list">لابردنی ئەژمێر لە لیستەکە</string>
|
||||
<string name="action_add_to_list">زیادکردنی ئەژمێر بۆ لیستەکە</string>
|
||||
<string name="hint_search_people_list">گەڕان بەدوای ئەو کەسانەی کە پەیڕەوی ان دەکەیت</string>
|
||||
<string name="action_delete_list">سڕینەوەی لیستەکە</string>
|
||||
<string name="action_rename_list">ناونانەوەی لیستەکە</string>
|
||||
<string name="action_create_list">دروستکردنی لیستێک</string>
|
||||
<string name="error_delete_list">نەیتوانی لیستەکە بسڕێتەوە</string>
|
||||
<string name="error_rename_list">نەیتوانی ناوی لیست بنووسرێ</string>
|
||||
<string name="error_create_list">نەیتوانی لیست دروست بکات</string>
|
||||
<string name="error_delete_list_fmt">%2$s: \"%1$s\" نەیتوانی لیستەکە بسڕێتەوە</string>
|
||||
<string name="error_rename_list_fmt">%2$s: \"%1$s\" نەیتوانی ناوی لیست بنووسرێ</string>
|
||||
<string name="error_create_list_fmt">%2$s: \"%1$s\" نەیتوانی لیست دروست بکات</string>
|
||||
<string name="title_lists">لیستەکان</string>
|
||||
<string name="action_lists">لیستەکان</string>
|
||||
<string name="add_account_description">زیادکردنی ئەژمێری ماتۆدۆنی نوێ</string>
|
||||
<string name="add_account_name">زیادکردنی ئەژمێر</string>
|
||||
<string name="filter_add_description">دەستەواژە بۆ فلتەر</string>
|
||||
|
|
|
@ -239,11 +239,10 @@
|
|||
<string name="filter_add_description">Fráze k filtrování</string>
|
||||
<string name="add_account_name">Přidat účet</string>
|
||||
<string name="add_account_description">Přidat nový účet Mastodon</string>
|
||||
<string name="action_lists">Seznamy</string>
|
||||
<string name="title_lists">Seznamy</string>
|
||||
<string name="error_create_list">Nelze vytvořit seznam</string>
|
||||
<string name="error_rename_list">Nelze přejmenovat seznam</string>
|
||||
<string name="error_delete_list">Nelze smazat seznam</string>
|
||||
<string name="error_create_list_fmt">Nelze vytvořit seznam \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Nelze přejmenovat seznam \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Nelze smazat seznam \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Vytvořit seznam</string>
|
||||
<string name="action_rename_list">Přejmenovat seznam</string>
|
||||
<string name="action_delete_list">Smazat seznam</string>
|
||||
|
|
|
@ -205,7 +205,6 @@
|
|||
<string name="replying_to">Yn ymateb i @%s</string>
|
||||
<string name="add_account_name">Ychwanegu Cyfrif</string>
|
||||
<string name="add_account_description">Ychwanegu Cyfrif Mastodon newydd</string>
|
||||
<string name="action_lists">Rhestrau</string>
|
||||
<string name="title_lists">Rhestrau</string>
|
||||
<string name="compose_active_account_description">Yn postio fel %1$s</string>
|
||||
<string name="action_set_caption">Gosod pennawd</string>
|
||||
|
@ -451,7 +450,7 @@
|
|||
<string name="failed_to_pin">Wedi methu pinio</string>
|
||||
<string name="failed_to_unpin">Wedi methu dadbinio</string>
|
||||
<string name="pref_title_http_proxy_port_message">Dylai\'r porth fod rhwng %d a %d</string>
|
||||
<string name="error_rename_list">Methu diweddaru\'r rhestr</string>
|
||||
<string name="error_rename_list_fmt">Methu diweddaru\'r rhestr \"%1$s\": %2$s</string>
|
||||
<string name="pref_default_post_language">Iaith bostio ragosodedig</string>
|
||||
<string name="description_post_bookmarked">Tudalnodiwyd</string>
|
||||
<string name="select_list_title">Dewiswch restr</string>
|
||||
|
@ -491,13 +490,13 @@
|
|||
<string name="notification_sign_up_description">Hysbysiadau am ddefnyddwyr newydd</string>
|
||||
<string name="notification_report_description">Hysbysiadau am adroddiadau cymedroli</string>
|
||||
<string name="filter_dialog_whole_word_description">Os yw\'r allweddair neu\'r ymadrodd yn alffaniwmerig yn unig, mi fydd ond yn cael ei osod os yw\'n cyfateb â\'r gair cyfan</string>
|
||||
<string name="error_create_list">Methu creu rhestr</string>
|
||||
<string name="error_create_list_fmt">Methu creu rhestr \"%1$s\": %2$s</string>
|
||||
<string name="set_focus_description">Tapiwch neu lusgo\'r cylch i ddewis y canolbwynt a fydd bob amser yn weladwy mewn lluniau bach.</string>
|
||||
<string name="description_poll">Pôl gyda dewisiadau: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
<string name="list">Rhestr</string>
|
||||
<string name="action_set_focus">Gosod pwynt ffocws</string>
|
||||
<string name="status_created_at_now">nawr</string>
|
||||
<string name="error_delete_list">Methu dileu\'r rhestr</string>
|
||||
<string name="error_delete_list_fmt">Methu dileu\'r rhestr \"%1$s\": %2$s</string>
|
||||
<string name="action_add_to_list">Ychwanegwch gyfrif at y rhestr</string>
|
||||
<string name="action_remove_from_list">Tynnu cyfrif o\'r rhestr</string>
|
||||
<string name="action_add_reaction">ychwanegu ymateb</string>
|
||||
|
|
|
@ -225,7 +225,6 @@
|
|||
<string name="filter_dialog_update_button">Aktualisieren</string>
|
||||
<string name="add_account_name">Konto hinzufügen</string>
|
||||
<string name="add_account_description">Neues Mastodon-Konto hinzufügen</string>
|
||||
<string name="action_lists">Listen</string>
|
||||
<string name="title_lists">Listen</string>
|
||||
<string name="action_create_list">Liste erstellen</string>
|
||||
<string name="action_rename_list">Liste aktualisieren</string>
|
||||
|
@ -286,9 +285,9 @@
|
|||
<string name="download_media">Dateien herunterladen</string>
|
||||
<string name="downloading_media">Dateien werden heruntergeladen</string>
|
||||
<string name="filter_add_description">zu filternder Ausdruck</string>
|
||||
<string name="error_create_list">Liste konnte nicht erstellt werden</string>
|
||||
<string name="error_rename_list">Liste konnte nicht aktualisiert werden</string>
|
||||
<string name="error_delete_list">Liste konnte nicht gelöscht werden</string>
|
||||
<string name="error_create_list_fmt">Liste konnte nicht erstellt werden \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Liste konnte nicht aktualisiert werden \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Liste konnte nicht gelöscht werden \"%1$s\": %2$s</string>
|
||||
<string name="hint_search_people_list">Suche nach Leuten, denen du folgst</string>
|
||||
<string name="action_remove_from_list">Konto aus der Liste entfernen</string>
|
||||
<string name="edit_hashtag_hint">Hashtag ohne #</string>
|
||||
|
@ -653,7 +652,7 @@
|
|||
<string name="pref_title_show_self_boosts">Selbst geteilte Beiträge anzeigen</string>
|
||||
<string name="title_tab_public_trending_statuses">Beiträge</string>
|
||||
<string name="pref_update_notification_frequency_once_per_version">Einmal pro Version</string>
|
||||
<string name="error_404_not_found">Dein Server unterstützt diese Funktion nicht</string>
|
||||
<string name="error_404_not_found_fmt">Dein Server unterstützt diese Funktion nicht: %1$s</string>
|
||||
<string name="pref_title_font_family">Schriftfamilie</string>
|
||||
<string name="action_translate">Übersetzen</string>
|
||||
<string name="update_dialog_title">Eine Aktualisierung ist verfügbar</string>
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
<string name="title_migration_relogin">Re-login for push notifications</string>
|
||||
<string name="title_drafts">Drafts</string>
|
||||
<string name="title_tab_public_trending_statuses">Posts</string>
|
||||
<string name="error_404_not_found">Your server does not support this feature</string>
|
||||
<string name="error_404_not_found_fmt">Your server does not support this feature: %1$s</string>
|
||||
<string name="error_unfollowing_hashtag_format">Error unfollowing #%s</string>
|
||||
<string name="error_following_hashtag_format">Error following #%s</string>
|
||||
<string name="title_announcements">Announcements</string>
|
||||
|
|
|
@ -238,11 +238,10 @@
|
|||
<string name="filter_add_description">Frazo filtrota</string>
|
||||
<string name="add_account_name">Aldoni konton</string>
|
||||
<string name="add_account_description">Aldoni novan Mastodon-konton</string>
|
||||
<string name="action_lists">Listoj</string>
|
||||
<string name="title_lists">Listoj</string>
|
||||
<string name="error_create_list">Ne povis krei la liston</string>
|
||||
<string name="error_rename_list">Ne povis ŝanĝi la nomon de la listo</string>
|
||||
<string name="error_delete_list">Ne povis forigi la liston</string>
|
||||
<string name="error_create_list_fmt">Ne povis krei la liston \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Ne povis ŝanĝi la nomon de la listo \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Ne povis forigi la liston \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Krei liston</string>
|
||||
<string name="action_rename_list">Ŝanĝi la nomon de la listo</string>
|
||||
<string name="action_delete_list">Forigi la liston</string>
|
||||
|
|
|
@ -216,7 +216,6 @@
|
|||
<string name="replying_to">Respondiendo a @%s</string>
|
||||
<string name="add_account_name">Añadir cuenta</string>
|
||||
<string name="add_account_description">Añadir cuenta de Mastodon</string>
|
||||
<string name="action_lists">Listas</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="compose_active_account_description">Publicar como %1$s</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
@ -338,9 +337,9 @@
|
|||
<string name="filter_edit_title">Editar filtro</string>
|
||||
<string name="filter_dialog_update_button">Actualizar</string>
|
||||
<string name="filter_add_description">Frase para filtrar</string>
|
||||
<string name="error_create_list">No se pudo crear la lista</string>
|
||||
<string name="error_rename_list">No se pudo renombrar la lista</string>
|
||||
<string name="error_delete_list">No se pudo eliminar la lista</string>
|
||||
<string name="error_create_list_fmt">No se pudo crear la lista \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">No se pudo renombrar la lista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">No se pudo eliminar la lista \"%1$s\": %2$s</string>
|
||||
<string name="action_delete_list">Eliminar la lista</string>
|
||||
<string name="hint_search_people_list">Buscar personas que sigues</string>
|
||||
<string name="description_post_media">Contenido: %s</string>
|
||||
|
@ -671,7 +670,7 @@
|
|||
<string name="announcement_date_updated">(Actualizado: %1$s)</string>
|
||||
<string name="title_tab_public_trending_statuses">Publicaciones</string>
|
||||
<string name="pref_update_notification_frequency_once_per_version">Una vez por versión</string>
|
||||
<string name="error_404_not_found">Su servidor no soporta esta función</string>
|
||||
<string name="error_404_not_found_fmt">Su servidor no soporta esta función: %1$s</string>
|
||||
<string name="pref_title_font_family">Familia de fuente</string>
|
||||
<string name="notification_listenable_worker_description">Notificaciones cuando Pachli está trabajando en el fondo</string>
|
||||
<string name="list_exclusive_label">Oculta de la cronología de inicio</string>
|
||||
|
@ -696,4 +695,4 @@
|
|||
<string name="pref_title_update_check_now">Buscar actualizaciones ahora</string>
|
||||
<string name="pref_update_check_no_updates">No hay actualizaciones disponibles</string>
|
||||
<string name="pref_update_next_scheduled_check">Próxima comprobación: %1$s</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -202,7 +202,6 @@
|
|||
<string name="replying_to">\@%s-(r)i erantzuten</string>
|
||||
<string name="add_account_name">Gehitu kontua</string>
|
||||
<string name="add_account_description">Mastodon kontua gehitu</string>
|
||||
<string name="action_lists">Zerrendak</string>
|
||||
<string name="title_lists">Zerrendak</string>
|
||||
<string name="compose_active_account_description">%1$s kontuarekin tut egiten</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
@ -300,9 +299,9 @@
|
|||
<string name="filter_dialog_whole_word">Hitz osoa</string>
|
||||
<string name="filter_dialog_whole_word_description">Gako-hitza edo esaldia alfanumerikoa denean bakarrik, hitz osoarekin bat datorrenean bakarrik aplikatuko da</string>
|
||||
<string name="filter_add_description">Iragazteko esaldia</string>
|
||||
<string name="error_create_list">Ezin izan da zerrenda sortu</string>
|
||||
<string name="error_rename_list">Ezin izan da zerrendaren izena aldatu</string>
|
||||
<string name="error_delete_list">Ezin izan da zerrenda ezabatu</string>
|
||||
<string name="error_create_list_fmt">Ezin izan da zerrenda sortu \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Ezin izan da zerrendaren izena aldatu \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Ezin izan da zerrenda ezabatu \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Zerrenda sortu</string>
|
||||
<string name="action_rename_list">Zerrenda berrizendatu</string>
|
||||
<string name="action_delete_list">Ezabatu zerrenda</string>
|
||||
|
|
|
@ -199,7 +199,6 @@
|
|||
<string name="replying_to">در حال پاسخ به @%s</string>
|
||||
<string name="add_account_name">افزودن حساب</string>
|
||||
<string name="add_account_description">افزودن حساب ماستودون جدید</string>
|
||||
<string name="action_lists">سیاههها</string>
|
||||
<string name="title_lists">سیاههها</string>
|
||||
<string name="compose_active_account_description">فرستادن از طرف %1$s</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
@ -294,9 +293,9 @@
|
|||
<string name="filter_dialog_remove_button">برداشتن</string>
|
||||
<string name="filter_dialog_update_button">بهروز رسانی</string>
|
||||
<string name="filter_dialog_whole_word">تمام واژه</string>
|
||||
<string name="error_create_list">نتوانست سیاهه را ایجاد کند</string>
|
||||
<string name="error_rename_list">نتوانست سیاهه را بهروز کند</string>
|
||||
<string name="error_delete_list">نتوانست سیاهه را حذف کند</string>
|
||||
<string name="error_create_list_fmt">%2$s: \"%1$s\" نتوانست سیاهه را ایجاد کند</string>
|
||||
<string name="error_rename_list_fmt">%2$s: \"%1$s\" نتوانست سیاهه را بهروز کند</string>
|
||||
<string name="error_delete_list_fmt">%2$s: \"%1$s\" نتوانست سیاهه را حذف کند</string>
|
||||
<string name="action_create_list">ایجاد سیاهه</string>
|
||||
<string name="action_rename_list">بهروز رسانیسیاهه</string>
|
||||
<string name="action_delete_list">حذف سیاهه</string>
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
<string name="unpin_action">Poista kiinnitys</string>
|
||||
<string name="action_remove">Poista</string>
|
||||
<string name="title_lists">Listat</string>
|
||||
<string name="action_lists">Listat</string>
|
||||
<string name="filter_dialog_update_button">Päivitä</string>
|
||||
<string name="filter_dialog_remove_button">Poista</string>
|
||||
<string name="post_media_audio">Ääni</string>
|
||||
|
@ -340,7 +339,7 @@
|
|||
<string name="visibility_public">Julkinen: Julkaise julkisille aikajanoille</string>
|
||||
<string name="hint_description">Kuvaus</string>
|
||||
<string name="notification_report_format">Uusi raportti %s</string>
|
||||
<string name="error_rename_list">Listaa ei voitu päivittää</string>
|
||||
<string name="error_rename_list_fmt">Listaa ei voitu päivittää \"%1$s\": %2$s</string>
|
||||
<string name="send_post_content_to">Jaa julkaisu…</string>
|
||||
<string name="confirmation_hashtag_muted">#%s hiljennetty</string>
|
||||
<string name="replying_to">Vastaus käyttäjälle @%s</string>
|
||||
|
@ -350,10 +349,10 @@
|
|||
<string name="pref_title_alway_open_spoiler">Avaa aina sisältövaroituksella varustetut julkaisut</string>
|
||||
<string name="title_tab_public_trending_statuses">Julkaisut</string>
|
||||
<string name="action_share_account_link">Jaa linkki tiliin</string>
|
||||
<string name="error_create_list">Listaa ei voitu luoda</string>
|
||||
<string name="error_create_list_fmt">Listaa ei voitu luoda \"%1$s\": %2$s</string>
|
||||
<string name="notification_summary_report_format">%s · %d julkaisua liitteenä</string>
|
||||
<string name="send_account_link_to">Jaa tilin URL…</string>
|
||||
<string name="error_404_not_found">Palvelimesi ei tue tätä ominaisuutta</string>
|
||||
<string name="error_404_not_found_fmt">Palvelimesi ei tue tätä ominaisuutta: %1$s</string>
|
||||
<string name="abbreviated_hours_ago">%dt</string>
|
||||
<string name="filter_expiration_format">%s (%s)</string>
|
||||
<string name="action_set_focus">Valitse keskipiste</string>
|
||||
|
@ -372,7 +371,7 @@
|
|||
<string name="pref_title_account_filter_keywords">Profiilit</string>
|
||||
<string name="action_add_or_remove_from_list">Lisää tai poista listalta</string>
|
||||
<string name="confirmation_domain_unmuted">%s näytetään</string>
|
||||
<string name="error_delete_list">Listaa ei voitu poistaa</string>
|
||||
<string name="error_delete_list_fmt">Listaa ei voitu poistaa \"%1$s\": %2$s</string>
|
||||
<string name="filter_dialog_whole_word">Koko sana</string>
|
||||
<string name="title_tab_public_trending_hashtags">Aihetunnisteet</string>
|
||||
<string name="error_muting_hashtag_format">Virhe mykistettäessä tiliä #%s</string>
|
||||
|
@ -677,4 +676,4 @@
|
|||
<string name="pref_title_update_check_now">Etsi päivitystä nyt</string>
|
||||
<string name="pref_update_next_scheduled_check">Seuraava ajoitettu tarkistus: %1$s</string>
|
||||
<string name="pref_update_check_no_updates">Ei päivityksiä tarjolla</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -239,11 +239,10 @@
|
|||
<string name="filter_add_description">Phrase à filtrer</string>
|
||||
<string name="add_account_name">Ajouter un compte</string>
|
||||
<string name="add_account_description">Ajouter un nouveau compte Mastodon</string>
|
||||
<string name="action_lists">Listes</string>
|
||||
<string name="title_lists">Listes</string>
|
||||
<string name="error_create_list">Impossible de créer la liste</string>
|
||||
<string name="error_rename_list">Impossible de renommer la liste</string>
|
||||
<string name="error_delete_list">Impossible de supprimer la liste</string>
|
||||
<string name="error_create_list_fmt">Impossible de créer la liste \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Impossible de renommer la liste \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Impossible de supprimer la liste \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Créer une liste</string>
|
||||
<string name="action_rename_list">Renommer la liste</string>
|
||||
<string name="action_delete_list">Supprimer la liste</string>
|
||||
|
@ -634,7 +633,7 @@
|
|||
<string name="action_translate">Traduire</string>
|
||||
<string name="error_generic_fmt">Une erreur s\'est produite : %s</string>
|
||||
<string name="error_network_fmt">Une erreur réseau s\'est produite : %s</string>
|
||||
<string name="error_404_not_found">Votre serveur ne prend pas en charge cette fonctionnalité</string>
|
||||
<string name="error_404_not_found_fmt">Votre serveur ne prend pas en charge cette fonctionnalité: %1$s</string>
|
||||
<string name="title_public_trending">Tendances</string>
|
||||
<string name="title_public_trending_links">Liens en tendance</string>
|
||||
<string name="title_tab_public_trending_links">Liens</string>
|
||||
|
@ -668,4 +667,4 @@
|
|||
<string name="notification_notification_worker">Récupération des notifications …</string>
|
||||
<string name="notification_prune_cache">Maintenance du cache …</string>
|
||||
<string name="announcement_date">%1$s %2$s</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
<string name="action_delete_list">Smyt de list fuort</string>
|
||||
<string name="action_rename_list">Neam de list om</string>
|
||||
<string name="action_create_list">Meitsje in list oan</string>
|
||||
<string name="error_delete_list">Koe list net fuortsmite</string>
|
||||
<string name="error_rename_list">Koe list net omneame</string>
|
||||
<string name="error_create_list">Koe list net oanmeitsje</string>
|
||||
<string name="error_delete_list_fmt">Koe list net fuortsmite \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Koe list net omneame \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">Koe list net oanmeitsje \"%1$s\": %2$s</string>
|
||||
<string name="title_lists">Listen</string>
|
||||
<string name="action_lists">Listen</string>
|
||||
<string name="add_account_description">Nij Mastodon Account Tafoegje</string>
|
||||
<string name="add_account_name">Account Tafoegje</string>
|
||||
<string name="filter_dialog_update_button">Fernije</string>
|
||||
|
|
|
@ -155,7 +155,6 @@
|
|||
<string name="error_network">Tharla earráid líonra! Seiceáil do nasc agus bain triail eile as!</string>
|
||||
<string name="error_generic">Tharla earráid.</string>
|
||||
<string name="title_lists">Liostaí</string>
|
||||
<string name="action_lists">Liostaí</string>
|
||||
<string name="action_reset_schedule">Athshocraigh</string>
|
||||
<string name="action_search">Cuardaigh</string>
|
||||
<string name="action_edit_profile">Cuir próifíl in eagar</string>
|
||||
|
@ -273,9 +272,9 @@
|
|||
<string name="failed_fetch_posts">Theip ar postálacha a ghabháil</string>
|
||||
<string name="report_description_1">Seolfar an tuarascáil chuig do mhodhnóir freastalaí. Féadfaidh tú míniú a thabhairt ar an bhfáth go bhfuil tú ag tuairisciú an chuntais seo thíos:</string>
|
||||
<string name="add_account_description">Cuir Cuntas Mastodon nua leis</string>
|
||||
<string name="error_create_list">Níorbh fhéidir liosta a chruthú</string>
|
||||
<string name="error_rename_list">Níorbh fhéidir an liosta a athainmniú</string>
|
||||
<string name="error_delete_list">Níorbh fhéidir an liosta a scriosadh</string>
|
||||
<string name="error_create_list_fmt">Níorbh fhéidir liosta a chruthú \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Níorbh fhéidir an liosta a athainmniú \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Níorbh fhéidir an liosta a scriosadh \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Cruthaigh liosta</string>
|
||||
<string name="action_rename_list">Athainmnigh an liosta</string>
|
||||
<string name="action_delete_list">Scrios an liosta</string>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="title_lists">Liostaichean</string>
|
||||
<string name="action_lists">Liostaichean</string>
|
||||
<string name="action_reset_schedule">Ath-shuidhich</string>
|
||||
<string name="action_search">Lorg</string>
|
||||
<string name="action_view_account_preferences">Roghainnean a’ chunntais</string>
|
||||
|
@ -241,9 +240,9 @@
|
|||
<string name="action_delete_list">Sguab às an liosta</string>
|
||||
<string name="action_rename_list">Ùraich an liosta</string>
|
||||
<string name="action_create_list">Cruthaich liosta</string>
|
||||
<string name="error_delete_list">Cha b’ urrainn dhuinn an liosta a sguabadh às</string>
|
||||
<string name="error_rename_list">Cha b’ urrainn dhuinn an liosta ùrachadh</string>
|
||||
<string name="error_create_list">Cha b’ urrainn dhuinn an liosta a chruthachadh</string>
|
||||
<string name="error_delete_list_fmt">Cha b’ urrainn dhuinn an liosta a sguabadh às \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Cha b’ urrainn dhuinn an liosta ùrachadh \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">Cha b’ urrainn dhuinn an liosta a chruthachadh \"%1$s\": %2$s</string>
|
||||
<string name="add_account_description">Cuir cunntas Mastodon ùr ris</string>
|
||||
<string name="add_account_name">Cuir cunntas ris</string>
|
||||
<string name="filter_add_description">An abairt ri chriathradh</string>
|
||||
|
|
|
@ -246,11 +246,10 @@
|
|||
<string name="action_delete_list">Eliminar a listaxe</string>
|
||||
<string name="action_rename_list">Actualizar a listaxe</string>
|
||||
<string name="action_create_list">Crear unha listaxe</string>
|
||||
<string name="error_delete_list">Non se puido eliminar a listaxe</string>
|
||||
<string name="error_rename_list">Non se actualizou a listaxe</string>
|
||||
<string name="error_create_list">Non se puido crear a listaxe</string>
|
||||
<string name="error_delete_list_fmt">Non se puido eliminar a listaxe \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Non se actualizou a listaxe \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">Non se puido crear a listaxe \"%1$s\": %2$s</string>
|
||||
<string name="title_lists">Listaxes</string>
|
||||
<string name="action_lists">Listaxes</string>
|
||||
<string name="add_account_description">Engadir unha nova conta Mastodon</string>
|
||||
<string name="add_account_name">Engadir conta</string>
|
||||
<string name="filter_add_description">Frase a filtrar</string>
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
<string name="error_empty">यह खाली नहीं हो सकता।</string>
|
||||
<string name="error_network">नेटवर्क त्रुटि हुई! कृपया अपना कनेक्शन जांचें और पुनः प्रयास करें!</string>
|
||||
<string name="title_lists">सूचियाँ</string>
|
||||
<string name="action_lists">सूचियाँ</string>
|
||||
<string name="description_poll">जनमत के विकल्प: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
<string name="action_view_bookmarks">बुकमार्क</string>
|
||||
<string name="edit_poll">संपादित करें</string>
|
||||
|
@ -290,9 +289,9 @@
|
|||
<string name="action_delete_list">सूची हटाएँ</string>
|
||||
<string name="action_rename_list">सूची का नाम बदलें</string>
|
||||
<string name="action_create_list">एक सूची बनाएं</string>
|
||||
<string name="error_delete_list">सूची नहीं हटाई जा सकी</string>
|
||||
<string name="error_rename_list">सूची का नाम नहीं बदल सके</string>
|
||||
<string name="error_create_list">सूची नहीं बना सके</string>
|
||||
<string name="error_delete_list_fmt">सूची नहीं हटाई जा सकी \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">सूची का नाम नहीं बदल सके \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">सूची नहीं बना सके \"%1$s\": %2$s</string>
|
||||
<string name="add_account_description">नया मास्टोडन खाता जोड़ें</string>
|
||||
<string name="filter_add_description">फ़िल्टर करने के लिए वाक्यांश</string>
|
||||
<string name="filter_dialog_whole_word_description">जब संकेत शब्द या वाक्यांश केवल अल्फ़ान्यूमेरिक होता है, तो यह केवल तभी लागू होगा जब यह पूरे शब्द से मेल खाता होगा</string>
|
||||
|
|
|
@ -207,7 +207,6 @@
|
|||
<string name="title_media">Média</string>
|
||||
<string name="add_account_name">Fiók hozzáadása</string>
|
||||
<string name="add_account_description">Új Mastodon-fiók hozzáadása</string>
|
||||
<string name="action_lists">Listák</string>
|
||||
<string name="title_lists">Listák</string>
|
||||
<string name="action_remove">Törlés</string>
|
||||
<string name="lock_account_label">Fiók zárolása</string>
|
||||
|
@ -275,9 +274,9 @@
|
|||
<string name="filter_dialog_remove_button">Eltávolítás</string>
|
||||
<string name="filter_dialog_update_button">Frissítés</string>
|
||||
<string name="filter_add_description">Szűrendő kifejezés</string>
|
||||
<string name="error_create_list">Nem sikerült a lista létrehozása</string>
|
||||
<string name="error_rename_list">Nem sikerült a lista átnevezése</string>
|
||||
<string name="error_delete_list">Nem sikerült a lista törlése</string>
|
||||
<string name="error_create_list_fmt">Nem sikerült a lista létrehozása \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Nem sikerült a lista átnevezése \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Nem sikerült a lista törlése \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Lista létrehozása</string>
|
||||
<string name="action_rename_list">Lista átnevezése</string>
|
||||
<string name="action_delete_list">Lista törlése</string>
|
||||
|
|
|
@ -115,10 +115,9 @@
|
|||
<string name="status_count_one_plus">1+</string>
|
||||
<string name="follows_you">Mengikuti Anda</string>
|
||||
<string name="add_account_description">Tambahkan Akun Mastodon baru</string>
|
||||
<string name="action_lists">Daftar</string>
|
||||
<string name="title_lists">Daftar</string>
|
||||
<string name="error_rename_list">Tidak dapat mengubah nama daftar</string>
|
||||
<string name="error_delete_list">Tidak dapat menghapus daftar</string>
|
||||
<string name="error_rename_list_fmt">Tidak dapat mengubah nama daftar \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Tidak dapat menghapus daftar \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Buat daftar</string>
|
||||
<string name="action_rename_list">Ubah nama daftar</string>
|
||||
<string name="action_delete_list">Hapus daftar</string>
|
||||
|
@ -149,7 +148,7 @@
|
|||
<string name="notification_favourite_name">Favorit</string>
|
||||
<string name="post_media_images">Gambar</string>
|
||||
<string name="add_account_name">Tambah Akun</string>
|
||||
<string name="error_create_list">Tidak dapat membuat daftar</string>
|
||||
<string name="error_create_list_fmt">Tidak dapat membuat daftar \"%1$s\": %2$s</string>
|
||||
<string name="later">Nanti</string>
|
||||
<string name="pachli_compose_post_quicksetting_label">Tulis Postingan</string>
|
||||
<string name="error_media_upload_image_or_video">Gambar dan video tidak dapat disematkan ke dalam post yang sama.</string>
|
||||
|
@ -243,7 +242,7 @@
|
|||
<string name="title_followed_hashtags">Tagar yang diikuti</string>
|
||||
<string name="error_media_upload_sending_fmt">Upload gagal: %s</string>
|
||||
<string name="title_tab_public_trending_statuses">Postingan</string>
|
||||
<string name="error_404_not_found">Server Anda tidak mendukung fitur ini</string>
|
||||
<string name="error_404_not_found_fmt">Server Anda tidak mendukung fitur ini: %1$s</string>
|
||||
<string name="title_tab_public_trending_hashtags">Tagar</string>
|
||||
<string name="notification_header_report_format">%s dilaporkan %s</string>
|
||||
<string name="dialog_follow_hashtag_title">Ikuti tagar</string>
|
||||
|
@ -288,4 +287,4 @@
|
|||
<string name="confirmation_hashtag_muted">#%s tersembunyi</string>
|
||||
<string name="confirmation_hashtag_unmuted">#%s tidak tersembunyi</string>
|
||||
<string name="confirmation_hashtag_unfollowed">#%s berhenti mengikuti</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<string name="action_view_account_preferences">Eiginleikar notandaaðgangs</string>
|
||||
<string name="action_edit_profile">Breyta notandasniði</string>
|
||||
<string name="action_search">Leita</string>
|
||||
<string name="action_lists">Listar</string>
|
||||
<string name="title_lists">Listar</string>
|
||||
<string name="error_generic">Villa kom upp.</string>
|
||||
<string name="error_network">Villa kom upp í netkerfi. Athugaðu nettenginguna þína og prófaðu svo aftur.</string>
|
||||
|
@ -258,9 +257,9 @@
|
|||
<string name="filter_add_description">Frasi sem á að sía</string>
|
||||
<string name="add_account_name">Bæta við aðgang</string>
|
||||
<string name="add_account_description">Bæta við nýjum Mastodon-aðgangi</string>
|
||||
<string name="error_create_list">Ekki tókst að búa til lista</string>
|
||||
<string name="error_rename_list">Ekki tókst að endurnefna lista</string>
|
||||
<string name="error_delete_list">Ekki tókst að eyða lista</string>
|
||||
<string name="error_create_list_fmt">Ekki tókst að búa til lista \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Ekki tókst að endurnefna lista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Ekki tókst að eyða lista \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Búa til lista</string>
|
||||
<string name="action_rename_list">Endurnefna listann</string>
|
||||
<string name="action_delete_list">Eyða listanum</string>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<string name="error_empty">Questo non può essere vuoto.</string>
|
||||
<string name="error_no_web_browser_found">Nessun browser web utilizzabile trovato.</string>
|
||||
<string name="error_compose_character_limit">Il messaggio è troppo lungo!</string>
|
||||
<string name="error_404_not_found">Il tuo server non supporta questa feature</string>
|
||||
<string name="error_404_not_found_fmt">Il tuo server non supporta questa feature: %1$s</string>
|
||||
<string name="error_media_upload_type">Quel tipo di file non può essere caricato.</string>
|
||||
<string name="error_media_upload_opening">Non è stato possibile aprire quel file.</string>
|
||||
<string name="error_media_upload_permission">È richiesto il permesso di leggere file.</string>
|
||||
|
@ -253,11 +253,10 @@
|
|||
<string name="filter_add_description">Frase da filtrare</string>
|
||||
<string name="add_account_name">Aggiungi account</string>
|
||||
<string name="add_account_description">Aggiungi un nuovo Account Mastodon</string>
|
||||
<string name="action_lists">Liste</string>
|
||||
<string name="title_lists">Liste</string>
|
||||
<string name="error_create_list">Non è stato possibile creare la lista</string>
|
||||
<string name="error_rename_list">Non è stato possibile rinominare la lista</string>
|
||||
<string name="error_delete_list">Non è stato possibile eliminare la lista</string>
|
||||
<string name="error_create_list_fmt">Non è stato possibile creare la lista \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Non è stato possibile rinominare la lista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Non è stato possibile eliminare la lista \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Crea una lista</string>
|
||||
<string name="action_rename_list">Rinomina la lista</string>
|
||||
<string name="action_delete_list">Elimina la lista</string>
|
||||
|
|
|
@ -221,9 +221,8 @@
|
|||
<string name="filter_edit_title">フィルターを編集</string>
|
||||
<string name="add_account_name">アカウントを追加</string>
|
||||
<string name="add_account_description">新しいMastodonアカウントを追加</string>
|
||||
<string name="action_lists">リスト</string>
|
||||
<string name="title_lists">リスト</string>
|
||||
<string name="error_rename_list">リスト名を変更できませんでした</string>
|
||||
<string name="error_rename_list_fmt">リスト名を変更できませんでした \"%1$s\": %2$s</string>
|
||||
<string name="action_rename_list">リスト名の変更</string>
|
||||
<string name="compose_active_account_description">%1$sとして投稿</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
@ -321,8 +320,8 @@
|
|||
<string name="dialog_redraft_post_warning">この投稿を削除し、下書きに戻しますか?</string>
|
||||
<string name="filter_dialog_remove_button">削除</string>
|
||||
<string name="filter_dialog_update_button">更新</string>
|
||||
<string name="error_create_list">リストを作成できませんでした</string>
|
||||
<string name="error_delete_list">リストを削除できませんでした</string>
|
||||
<string name="error_create_list_fmt">リストを作成できませんでした \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">リストを削除できませんでした \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">リストの作成</string>
|
||||
<string name="action_delete_list">リストの削除</string>
|
||||
<string name="hint_search_people_list">フォロワーを検索</string>
|
||||
|
@ -616,7 +615,7 @@
|
|||
<string name="dialog_save_profile_changes_message">プロフィールの変更を保存しますか\?</string>
|
||||
<string name="error_generic_fmt">エラーが発生しました: %s</string>
|
||||
<string name="title_tab_public_trending_statuses">投稿</string>
|
||||
<string name="error_404_not_found">あなたのサーバーはこの機能をサポートしていません</string>
|
||||
<string name="error_404_not_found_fmt">あなたのサーバーはこの機能をサポートしていません: %1$s</string>
|
||||
<string name="title_tab_public_trending_hashtags">ハッシュタグ</string>
|
||||
<string name="dialog_delete_filter_positive_action">削除する</string>
|
||||
<string name="reaction_name_and_count">%1$s %2$d</string>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<string name="action_view_account_preferences">Iɣewwaṛen n umiḍan</string>
|
||||
<string name="action_edit_profile">Ẓreg amaɣnu</string>
|
||||
<string name="action_search">Nadi</string>
|
||||
<string name="action_lists">Tabdart</string>
|
||||
<string name="title_lists">Tabdarin</string>
|
||||
<string name="error_compose_character_limit">Izen-ik·im aṭas i ɣezzif!</string>
|
||||
<string name="title_home">Agejdan</string>
|
||||
|
|
|
@ -252,11 +252,10 @@
|
|||
<string name="filter_add_description">필터링할 문구 입력</string>
|
||||
<string name="add_account_name">계정 추가</string>
|
||||
<string name="add_account_description">마스토돈 계정을 추가합니다</string>
|
||||
<string name="action_lists">리스트</string>
|
||||
<string name="title_lists">리스트</string>
|
||||
<string name="error_create_list">리스트를 만들 수 없습니다.</string>
|
||||
<string name="error_rename_list">리스트의 이름을 변경할 수 없습니다.</string>
|
||||
<string name="error_delete_list">리스트를 삭제할 수 없습니다.</string>
|
||||
<string name="error_create_list_fmt">리스트를 만들 수 없습니다. \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">리스트의 이름을 변경할 수 없습니다. \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">리스트를 삭제할 수 없습니다. \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">리스트 생성</string>
|
||||
<string name="action_rename_list">리스트 이름 바꾸기</string>
|
||||
<string name="action_delete_list">리스트 삭제</string>
|
||||
|
|
|
@ -110,11 +110,10 @@
|
|||
<string name="pref_title_thread_filter_keywords">Sarunas</string>
|
||||
<string name="filter_dialog_remove_button">Noņemt</string>
|
||||
<string name="filter_dialog_update_button">Atjaunināt</string>
|
||||
<string name="action_lists">Saraksti</string>
|
||||
<string name="title_lists">Saraksti</string>
|
||||
<string name="filter_edit_title">Labot filtru</string>
|
||||
<string name="add_account_name">Pievienot kontu</string>
|
||||
<string name="error_delete_list">Nevarēja dzēst sarakstu</string>
|
||||
<string name="error_delete_list_fmt">Nevarēja dzēst sarakstu \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Izveidot sarakstu</string>
|
||||
<string name="action_rename_list">Pārsaukt sarakstu</string>
|
||||
<string name="action_delete_list">Dzēst sarakstu</string>
|
||||
|
@ -174,7 +173,7 @@
|
|||
<string name="delete_scheduled_post_warning">Vai dzēst šo ieplānoto ierakstu\?</string>
|
||||
<string name="error_generic">Notika kļūda.</string>
|
||||
<string name="error_media_upload_sending">Augšupielāde neizdevās.</string>
|
||||
<string name="error_rename_list">Nevarēja pārsaukt sarakstu</string>
|
||||
<string name="error_rename_list_fmt">Nevarēja pārsaukt sarakstu \"%1$s\": %2$s</string>
|
||||
<string name="action_add_or_remove_from_list">Pievienot vai noņemt no saraksta</string>
|
||||
<string name="edit_poll">Labot</string>
|
||||
<string name="action_access_drafts">Melnraksti</string>
|
||||
|
@ -435,7 +434,7 @@
|
|||
<string name="a11y_label_loading_thread">Ielādē pavedienu</string>
|
||||
<string name="confirmation_hashtag_unfollowed">pārtraukta sekošana #%s</string>
|
||||
<string name="confirmation_domain_unmuted">%s atcelta slēpšana</string>
|
||||
<string name="error_create_list">Nevarēja izveidot sarakstu</string>
|
||||
<string name="error_create_list_fmt">Nevarēja izveidot sarakstu \"%1$s\": %2$s</string>
|
||||
<string name="filter_add_description">Filtrējamā frāze</string>
|
||||
<string name="pref_summary_http_proxy_disabled">Atspējots</string>
|
||||
<string name="pref_summary_http_proxy_missing"><nav iestatīts></string>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<string name="action_view_account_preferences">അക്കൗണ്ട് മുൻഗണനകൾ</string>
|
||||
<string name="action_edit_profile">പ്രൊഫൈൽ തിരുത്തുക</string>
|
||||
<string name="action_search">തിരയുക</string>
|
||||
<string name="action_lists">പട്ടികകൾ</string>
|
||||
<string name="title_lists">പട്ടികകൾ</string>
|
||||
<string name="error_generic">ഒരു പിഴവ് സംഭവിച്ചിരിക്കുന്നു.</string>
|
||||
<string name="error_network">ഒരു നെറ്റ്വർക്ക് പിഴവ് സംഭവിച്ചിരിക്കുന്നു! ദയവായി താങ്കളുടെ കണക്ഷൻ പരിശോധിച്ചിട്ട് വീണ്ടും ശ്രമിക്കൂ!</string>
|
||||
|
|
|
@ -214,11 +214,10 @@
|
|||
<string name="filter_add_description">Filtrer frase</string>
|
||||
<string name="add_account_name">Legg til konto</string>
|
||||
<string name="add_account_description">Legg til ny Mastodon-konto</string>
|
||||
<string name="action_lists">Lister</string>
|
||||
<string name="title_lists">Lister</string>
|
||||
<string name="error_create_list">Kunne ikke opprette liste</string>
|
||||
<string name="error_rename_list">Kunne ikke gi liste nytt navn</string>
|
||||
<string name="error_delete_list">Kunne ikke slette liste</string>
|
||||
<string name="error_create_list_fmt">Kunne ikke opprette liste \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Kunne ikke gi liste nytt navn \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Kunne ikke slette liste \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Opprett en liste</string>
|
||||
<string name="action_rename_list">Gi listen nytt navn</string>
|
||||
<string name="action_delete_list">Fjern listen</string>
|
||||
|
|
|
@ -228,7 +228,6 @@
|
|||
<string name="replying_to">Aan het reageren op @%s</string>
|
||||
<string name="add_account_name">Account toevoegen</string>
|
||||
<string name="add_account_description">Een nieuw Mastodonaccount toevoegen</string>
|
||||
<string name="action_lists">Lijsten</string>
|
||||
<string name="title_lists">Lijsten</string>
|
||||
<string name="compose_active_account_description">Berichten plaatsen als %1$s</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
@ -303,9 +302,9 @@
|
|||
<string name="filter_dialog_remove_button">Verwijderen</string>
|
||||
<string name="filter_dialog_update_button">Bijwerken</string>
|
||||
<string name="filter_add_description">Zinsdeel om te filteren</string>
|
||||
<string name="error_create_list">Kon geen lijst aanmaken</string>
|
||||
<string name="error_rename_list">Kan lijst niet updaten</string>
|
||||
<string name="error_delete_list">Kon de lijst niet verwijderen</string>
|
||||
<string name="error_create_list_fmt">Kon geen lijst aanmaken \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Kan lijst niet updaten \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Kon de lijst niet verwijderen \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Lijst aanmaken</string>
|
||||
<string name="action_rename_list">Lijst updaten</string>
|
||||
<string name="action_delete_list">Lijst verwijderen</string>
|
||||
|
@ -656,7 +655,7 @@
|
|||
\n
|
||||
\nOm meer accounts te vinden, kun je ze ontdekken in een van de andere tijdlijnen. Bijvoorbeeld de lokale tijdlijn van jouw instance [iconics gmd_group]. Of je kunt zoeken op naam [iconics gmd_search]; bijvoorbeeld als je zoekt op Pachli dan vind je ons Mastodon account.</string>
|
||||
<string name="pref_update_notification_frequency_once_per_version">Eenmaal per versie</string>
|
||||
<string name="error_404_not_found">Je server beschikt niet over ondersteuning voor deze feature</string>
|
||||
<string name="error_404_not_found_fmt">Je server beschikt niet over ondersteuning voor deze feature: %1$s</string>
|
||||
<string name="pref_title_font_family">Lettertype-familie</string>
|
||||
<string name="list_exclusive_label">Verberg op de Thuis tijdlijn</string>
|
||||
<string name="ui_success_rejected_follow_request">Volg verzoek geblokkeerd</string>
|
||||
|
@ -684,4 +683,4 @@
|
|||
<string name="pref_update_check_no_updates">Er zijn geen updates beschikbaar</string>
|
||||
<string name="pref_update_next_scheduled_check">Volgende geplande controle: %1$s</string>
|
||||
<string name="pref_summary_timeline_filters">Je server ondersteund geen filters</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -194,7 +194,6 @@
|
|||
<string name="replying_to">En responsa a @%s</string>
|
||||
<string name="add_account_name">Apondre un compte</string>
|
||||
<string name="add_account_description">Apondre un nòu compte Mastodon</string>
|
||||
<string name="action_lists">Listas</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="compose_active_account_description">Publicar coma %1$s</string>
|
||||
<string name="action_set_caption">Apondre una legenda</string>
|
||||
|
@ -276,9 +275,9 @@
|
|||
<string name="filter_dialog_remove_button">Suprimir</string>
|
||||
<string name="filter_dialog_update_button">Actualizar</string>
|
||||
<string name="filter_add_description">Frasa de filtrar</string>
|
||||
<string name="error_create_list">Creacion impossibla de la lsita</string>
|
||||
<string name="error_rename_list">Impossible de renomenar la lista</string>
|
||||
<string name="error_delete_list">Supression impossibla de la lista</string>
|
||||
<string name="error_create_list_fmt">Creacion impossibla de la lsita \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Impossible de renomenar la lista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Supression impossibla de la lista \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Crear una lista</string>
|
||||
<string name="action_rename_list">Renomenar la lista</string>
|
||||
<string name="action_delete_list">Suprimir la lista</string>
|
||||
|
|
|
@ -198,7 +198,6 @@
|
|||
<string name="replying_to">Odpowiadasz na wpis autorstwa @%s</string>
|
||||
<string name="add_account_name">Dodaj konto</string>
|
||||
<string name="add_account_description">Dodaj nowe Konto Mastodon</string>
|
||||
<string name="action_lists">Listy</string>
|
||||
<string name="title_lists">Listy</string>
|
||||
<string name="compose_active_account_description">Publikowanie jako %1$s</string>
|
||||
<string name="action_set_caption">Ustaw podpis</string>
|
||||
|
@ -292,9 +291,9 @@
|
|||
<string name="filter_dialog_whole_word">Całe słowo</string>
|
||||
<string name="filter_dialog_whole_word_description">Kiedy słowo kluczowe lub fraza jest tylko alfanumeryczna, filtr będzie zastosowany jeśli pasuje do całego słowa</string>
|
||||
<string name="filter_add_description">Fraza, która ma być filtrowana</string>
|
||||
<string name="error_create_list">Tworzenie listy nie powiodło się</string>
|
||||
<string name="error_rename_list">Zmiana nazwy listy nie powiodła się</string>
|
||||
<string name="error_delete_list">Usunięcie listy nie powiodło się</string>
|
||||
<string name="error_create_list_fmt">Tworzenie listy nie powiodło się \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Zmiana nazwy listy nie powiodła się \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Usunięcie listy nie powiodło się \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Stwórz listę</string>
|
||||
<string name="action_rename_list">Zmień nazwę listy</string>
|
||||
<string name="action_delete_list">Usuń listę</string>
|
||||
|
|
|
@ -216,7 +216,6 @@
|
|||
<string name="replying_to">Respondendo @%s</string>
|
||||
<string name="add_account_name">Adicionar conta</string>
|
||||
<string name="add_account_description">Adicionar nova conta Mastodon</string>
|
||||
<string name="action_lists">Listas</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="compose_active_account_description">Postando como %1$s</string>
|
||||
<string name="action_set_caption">Descrever</string>
|
||||
|
@ -284,9 +283,9 @@
|
|||
<string name="filter_dialog_remove_button">Excluir</string>
|
||||
<string name="filter_dialog_update_button">Atualizar</string>
|
||||
<string name="filter_add_description">Frase para filtrar</string>
|
||||
<string name="error_create_list">Não foi possível criar a lista</string>
|
||||
<string name="error_rename_list">Não foi possível renomear a lista</string>
|
||||
<string name="error_delete_list">Não foi possível excluir a lista</string>
|
||||
<string name="error_create_list_fmt">Não foi possível criar a lista \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Não foi possível renomear a lista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Não foi possível excluir a lista \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Criar uma lista</string>
|
||||
<string name="action_rename_list">Renomear lista</string>
|
||||
<string name="action_delete_list">Excluir lista</string>
|
||||
|
@ -650,7 +649,7 @@
|
|||
<string name="notification_summary_report_format">%s · %d Toots anexados</string>
|
||||
<string name="send_account_link_to">Compartilhar URL da conta para…</string>
|
||||
<string name="post_media_image">Imagem</string>
|
||||
<string name="error_404_not_found">Tua instância não suporta este recurso</string>
|
||||
<string name="error_404_not_found_fmt">Tua instância não suporta este recurso: %1$s</string>
|
||||
<string name="filter_action_hide">Ocultar</string>
|
||||
<string name="label_filter_title">Nome</string>
|
||||
<string name="pref_title_font_family">Família da fonte</string>
|
||||
|
|
|
@ -292,11 +292,10 @@
|
|||
<string name="filter_add_description">Frase para filtrar</string>
|
||||
<string name="add_account_name">Adicionar Conta</string>
|
||||
<string name="add_account_description">Adicionar nova Conta Mastodon</string>
|
||||
<string name="action_lists">Listas</string>
|
||||
<string name="error_rename_list">Não foi possível renomear a lista</string>
|
||||
<string name="error_rename_list_fmt">Não foi possível renomear a lista \"%1$s\": %2$s</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="error_create_list">Não foi possível criar a lista</string>
|
||||
<string name="error_delete_list">Não foi possível apagar a lista</string>
|
||||
<string name="error_create_list_fmt">Não foi possível criar a lista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Não foi possível apagar a lista \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Criar uma lista</string>
|
||||
<string name="action_rename_list">Renomear a lista</string>
|
||||
<string name="action_delete_list">Apagar a lista</string>
|
||||
|
|
|
@ -264,11 +264,10 @@
|
|||
<string name="filter_add_description">Слова на фильтр</string>
|
||||
<string name="add_account_name">Добавить аккаунт</string>
|
||||
<string name="add_account_description">Добавить новый акканут Mastodon</string>
|
||||
<string name="action_lists">Списки</string>
|
||||
<string name="title_lists">Списки</string>
|
||||
<string name="error_create_list">Не удалось создать список</string>
|
||||
<string name="error_rename_list">Не удалось переименовать список</string>
|
||||
<string name="error_delete_list">Не удалось удалить список</string>
|
||||
<string name="error_create_list_fmt">Не удалось создать список \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Не удалось переименовать список \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Не удалось удалить список \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Создать список</string>
|
||||
<string name="action_rename_list">Переименовать список</string>
|
||||
<string name="action_delete_list">Удалить список</string>
|
||||
|
@ -494,4 +493,4 @@
|
|||
<string name="title_edits">Правки</string>
|
||||
<string name="status_edit_info">%1$s отредактировали</string>
|
||||
<string name="status_created_info">%1$s создали</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -173,12 +173,11 @@
|
|||
<string name="action_delete_list">सूचिर्नश्यताम्</string>
|
||||
<string name="action_rename_list">पुनः सूचिनामकरणं क्रियताम्</string>
|
||||
<string name="action_create_list">सूचिः निर्मीयताम्</string>
|
||||
<string name="error_delete_list">सूचिर्नष्टुमशक्या</string>
|
||||
<string name="error_rename_list">पुनः सूचिनामकरणं कर्तुमशक्यम्</string>
|
||||
<string name="error_create_list">सूचिनिर्माणं कर्तुमशक्यम्</string>
|
||||
<string name="error_delete_list_fmt">सूचिर्नष्टुमशक्या \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">पुनः सूचिनामकरणं कर्तुमशक्यम् \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">सूचिनिर्माणं कर्तुमशक्यम् \"%1$s\": %2$s</string>
|
||||
<string name="dialog_message_cancel_follow_request">अनुसरणानुरोधो नश्यताम् \?</string>
|
||||
<string name="title_lists">सूचयः</string>
|
||||
<string name="action_lists">सूचयः</string>
|
||||
<string name="add_account_description">नवमास्टोडोनलेखा युज्यताम्</string>
|
||||
<string name="add_account_name">नवलेखा युज्यताम्</string>
|
||||
<string name="filter_add_description">शोधनार्थं वाक्यांशः</string>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<string name="action_view_account_preferences">Nastavenia účtu</string>
|
||||
<string name="action_edit_profile">Upraviť profil</string>
|
||||
<string name="action_search">Hľadať</string>
|
||||
<string name="action_lists">Zoznamy</string>
|
||||
<string name="title_lists">Zoznamy</string>
|
||||
<string name="error_generic">Vyskytla sa chyba.</string>
|
||||
<string name="title_notifications">Oznámenia</string>
|
||||
|
|
|
@ -216,11 +216,10 @@
|
|||
<string name="filter_add_description">Filtriraj frazo</string>
|
||||
<string name="add_account_name">Dodaj račun</string>
|
||||
<string name="add_account_description">Dodaj nov Mastodon račun</string>
|
||||
<string name="action_lists">Seznami</string>
|
||||
<string name="title_lists">Seznami</string>
|
||||
<string name="error_create_list">Seznama ni bilo mogoče ustvariti</string>
|
||||
<string name="error_rename_list">Seznama ni bilo mogoče preimenovati</string>
|
||||
<string name="error_delete_list">Seznama ni bilo mogoče izbrisati</string>
|
||||
<string name="error_create_list_fmt">Seznama ni bilo mogoče ustvariti \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Seznama ni bilo mogoče preimenovati \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Seznama ni bilo mogoče izbrisati \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Ustvari seznam</string>
|
||||
<string name="action_rename_list">Preimenuj seznam</string>
|
||||
<string name="action_delete_list">Izbriši seznam</string>
|
||||
|
|
|
@ -236,11 +236,10 @@
|
|||
<string name="filter_add_description">Filtrera fras</string>
|
||||
<string name="add_account_name">Lägg till konto</string>
|
||||
<string name="add_account_description">Lägg till ett nytt Mastodon-konto</string>
|
||||
<string name="action_lists">Listor</string>
|
||||
<string name="title_lists">Listor</string>
|
||||
<string name="error_create_list">Kunde inte skapa lista</string>
|
||||
<string name="error_rename_list">Kunde inte byta namn på lista</string>
|
||||
<string name="error_delete_list">Kunde inte radera lista</string>
|
||||
<string name="error_create_list_fmt">Kunde inte skapa lista \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Kunde inte byta namn på lista \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Kunde inte radera lista \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Skapa en lista</string>
|
||||
<string name="action_rename_list">Byt namn på listan</string>
|
||||
<string name="action_delete_list">Ta bort listan</string>
|
||||
|
@ -663,7 +662,7 @@
|
|||
<string name="announcement_date_updated">(Uppdaterad: %1$s)</string>
|
||||
<string name="title_tab_public_trending_statuses">Toots</string>
|
||||
<string name="pref_update_notification_frequency_once_per_version">En gång per version</string>
|
||||
<string name="error_404_not_found">Din server stöder inte denna funktion</string>
|
||||
<string name="error_404_not_found_fmt">Din server stöder inte denna funktion: %1$s</string>
|
||||
<string name="pref_title_font_family">Fontfamilj</string>
|
||||
<string name="list_exclusive_label">Dölj från hemtidlinjen</string>
|
||||
<string name="action_translate">Översätta</string>
|
||||
|
|
|
@ -188,7 +188,6 @@
|
|||
<string name="replying_to">\@%s -க்கு பதிலளி</string>
|
||||
<string name="add_account_name">கணக்கை சேர்க்க</string>
|
||||
<string name="add_account_description">புதிய Mastodon கணக்கைச் சேர்க்க</string>
|
||||
<string name="action_lists">பட்டியல்கள்</string>
|
||||
<string name="title_lists">பட்டியல்கள்</string>
|
||||
<string name="compose_active_account_description">%1$s கணக்குடன் பதிவிட</string>
|
||||
<string name="action_set_caption">தலைப்பை அமை</string>
|
||||
|
|
|
@ -129,9 +129,9 @@
|
|||
<string name="action_delete_list">ลบรายการ</string>
|
||||
<string name="action_rename_list">เปลี่ยนชื่อรายการ</string>
|
||||
<string name="action_create_list">สร้างรายการ</string>
|
||||
<string name="error_delete_list">ไม่สามารถลบรายการได้</string>
|
||||
<string name="error_rename_list">ไม่สามารถเปลี่ยนชื่อรายการได้</string>
|
||||
<string name="error_create_list">ไม่สามารถสร้างรายการได้</string>
|
||||
<string name="error_delete_list_fmt">ไม่สามารถลบรายการได้ \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">ไม่สามารถเปลี่ยนชื่อรายการได้ \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">ไม่สามารถสร้างรายการได้ \"%1$s\": %2$s</string>
|
||||
<string name="add_account_description">เพิ่มบัญชี Mastodon ใหม่</string>
|
||||
<string name="add_account_name">เพิ่มบัญชี</string>
|
||||
<string name="filter_add_description">วลีที่ต้องการกรอง</string>
|
||||
|
@ -388,7 +388,6 @@
|
|||
<string name="error_network">เกิดข้อผิดพลาดเครือข่าย! กรุณาตรวจสอบการเชื่อมต่อและลองอีกครั้ง!</string>
|
||||
<string name="error_generic">เกิดข้อผิดพลาด</string>
|
||||
<string name="title_lists">รายการ</string>
|
||||
<string name="action_lists">รายการ</string>
|
||||
<string name="action_reset_schedule">ล้างค่า</string>
|
||||
<string name="action_search">ค้นหา</string>
|
||||
<string name="action_edit_profile">แก้ไขโปรไฟล์</string>
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="title_media">Medya</string>
|
||||
<string name="add_account_name">Hesap Ekle</string>
|
||||
<string name="add_account_description">Yeni Mastodon hesabı ekle</string>
|
||||
<string name="action_lists">Listeler</string>
|
||||
<string name="title_lists">Listeler</string>
|
||||
<string name="compose_active_account_description">%1$s hesabıyla gönderiliyor</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
@ -297,9 +296,9 @@
|
|||
<string name="filter_dialog_whole_word">Tüm dünya</string>
|
||||
<string name="filter_dialog_whole_word_description">Bir anahtar kelime veya kelime öbeği sadece alfanümerik olduğunda, yalnızca tüm kelimeyle eşleşirse uygulanır</string>
|
||||
<string name="filter_add_description">Süzgeçlenecek ifade</string>
|
||||
<string name="error_create_list">Liste oluşturulamadı</string>
|
||||
<string name="error_rename_list">Liste oluşturulamadı</string>
|
||||
<string name="error_delete_list">Liste silinemedi</string>
|
||||
<string name="error_create_list_fmt">Liste oluşturulamadı \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Liste oluşturulamadı \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Liste silinemedi \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Liste oluştur</string>
|
||||
<string name="action_rename_list">Listeyi güncelle</string>
|
||||
<string name="action_delete_list">Listeyi sil</string>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
<string name="error_empty">Не може бути порожнім.</string>
|
||||
<string name="error_network">Сталася помилка мережі. Перевірте інтернет-з\'єднання та спробуйте знову.</string>
|
||||
<string name="title_lists">Списки</string>
|
||||
<string name="action_lists">Списки</string>
|
||||
<string name="action_reset_schedule">Скинути</string>
|
||||
<string name="action_search">Пошук</string>
|
||||
<string name="action_edit_profile">Редагувати профіль</string>
|
||||
|
@ -262,9 +261,9 @@
|
|||
<string name="action_delete_list">Видалити список</string>
|
||||
<string name="action_rename_list">Оновити список</string>
|
||||
<string name="action_create_list">Створити список</string>
|
||||
<string name="error_delete_list">Не вдалося видалити список</string>
|
||||
<string name="error_rename_list">Не вдалося оновити список</string>
|
||||
<string name="error_create_list">Не вдалося створити список</string>
|
||||
<string name="error_delete_list_fmt">Не вдалося видалити список \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Не вдалося оновити список \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">Не вдалося створити список \"%1$s\": %2$s</string>
|
||||
<string name="add_account_description">Додати новий обліковий запис Mastodon</string>
|
||||
<string name="add_account_name">Додати обліковий запис</string>
|
||||
<string name="filter_add_description">Фільтрувати фразу</string>
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
<string name="notification_reblog_format">%s đăng lại tút của bạn</string>
|
||||
<string name="error_no_custom_emojis">Máy chủ %s không có emoji tùy chỉnh</string>
|
||||
<string name="send_post_notification_error_title">Lỗi đăng tút</string>
|
||||
<string name="error_delete_list">Không thể xóa danh sách</string>
|
||||
<string name="error_rename_list">Không thể cập nhật danh sách</string>
|
||||
<string name="error_create_list">Không thể tạo danh sách</string>
|
||||
<string name="error_delete_list_fmt">Không thể xóa danh sách \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Không thể cập nhật danh sách \"%1$s\": %2$s</string>
|
||||
<string name="error_create_list_fmt">Không thể tạo danh sách \"%1$s\": %2$s</string>
|
||||
<string name="error_sender_account_gone">Xảy ra lỗi khi đăng tút.</string>
|
||||
<string name="error_media_upload_sending">Tải lên không thành công.</string>
|
||||
<string name="error_media_upload_image_or_video">Không thể đính kèm ảnh và video cùng một lúc.</string>
|
||||
|
@ -35,7 +35,6 @@
|
|||
<string name="error_network">Rớt mạng! Xin kiểm tra kết nối và thử lại!</string>
|
||||
<string name="error_generic">Đã có lỗi xảy ra.</string>
|
||||
<string name="title_lists">Danh sách</string>
|
||||
<string name="action_lists">Danh sách</string>
|
||||
<string name="action_reset_schedule">Làm tươi</string>
|
||||
<string name="action_search">Tìm kiếm</string>
|
||||
<string name="action_edit_profile">Hồ sơ</string>
|
||||
|
|
|
@ -242,11 +242,10 @@
|
|||
<string name="filter_add_description">需要过滤的文字</string>
|
||||
<string name="add_account_name">添加账号</string>
|
||||
<string name="add_account_description">添加新的 Mastodon 账号</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="error_create_list">无法新建列表</string>
|
||||
<string name="error_rename_list">无法更新列表</string>
|
||||
<string name="error_delete_list">无法删除列表</string>
|
||||
<string name="error_create_list_fmt">无法新建列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">无法更新列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">无法删除列表 \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">新建列表</string>
|
||||
<string name="action_rename_list">更新列表</string>
|
||||
<string name="action_delete_list">删除列表</string>
|
||||
|
|
|
@ -241,11 +241,10 @@
|
|||
<string name="filter_add_description">需要過濾的文字</string>
|
||||
<string name="add_account_name">加入帳號</string>
|
||||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="error_create_list">無法新建列表</string>
|
||||
<string name="error_rename_list">無法重命名列表</string>
|
||||
<string name="error_delete_list">無法刪除列表</string>
|
||||
<string name="error_create_list_fmt">無法新建列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">無法重命名列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">無法刪除列表 \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">新建列表</string>
|
||||
<string name="action_rename_list">重命名列表</string>
|
||||
<string name="action_delete_list">刪除列表</string>
|
||||
|
|
|
@ -241,11 +241,10 @@
|
|||
<string name="filter_add_description">需要過濾的文字</string>
|
||||
<string name="add_account_name">加入帳號</string>
|
||||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="error_create_list">無法新建列表</string>
|
||||
<string name="error_rename_list">無法重命名列表</string>
|
||||
<string name="error_delete_list">無法刪除列表</string>
|
||||
<string name="error_create_list_fmt">無法新建列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">無法重命名列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">無法刪除列表 \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">新建列表</string>
|
||||
<string name="action_rename_list">重命名列表</string>
|
||||
<string name="action_delete_list">刪除列表</string>
|
||||
|
|
|
@ -242,11 +242,10 @@
|
|||
<string name="filter_add_description">需要过滤的文字</string>
|
||||
<string name="add_account_name">添加账号</string>
|
||||
<string name="add_account_description">添加新的 Mastodon 账号</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="error_create_list">无法新建列表</string>
|
||||
<string name="error_rename_list">无法重命名列表</string>
|
||||
<string name="error_delete_list">无法删除列表</string>
|
||||
<string name="error_create_list_fmt">无法新建列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">无法重命名列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">无法删除列表 \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">新建列表</string>
|
||||
<string name="action_rename_list">重命名列表</string>
|
||||
<string name="action_delete_list">删除列表</string>
|
||||
|
|
|
@ -241,11 +241,10 @@
|
|||
<string name="filter_add_description">需要過濾的文字</string>
|
||||
<string name="add_account_name">加入帳號</string>
|
||||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="error_create_list">無法新建列表</string>
|
||||
<string name="error_rename_list">無法重命名列表</string>
|
||||
<string name="error_delete_list">無法刪除列表</string>
|
||||
<string name="error_create_list_fmt">無法新建列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">無法重命名列表 \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">無法刪除列表 \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">新建列表</string>
|
||||
<string name="action_rename_list">重命名列表</string>
|
||||
<string name="action_delete_list">刪除列表</string>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<string name="error_no_web_browser_found">Couldn\'t find a web browser to use.</string>
|
||||
<string name="error_compose_character_limit">The post is too long!</string>
|
||||
<string name="error_multimedia_size_limit">Video and audio files cannot exceed %s MB in size.</string>
|
||||
<string name="error_404_not_found">Your server does not support this feature</string>
|
||||
<string name="error_404_not_found_fmt">Your server does not support this feature: %1$s</string>
|
||||
<string name="error_image_edit_failed">The image could not be edited.</string>
|
||||
<string name="error_media_upload_type">That type of file cannot be uploaded.</string>
|
||||
<string name="error_media_upload_opening">That file could not be opened.</string>
|
||||
|
@ -392,11 +392,13 @@
|
|||
<string name="filter_expiration_format">%s (%s)</string>
|
||||
<string name="add_account_name">Add Account</string>
|
||||
<string name="add_account_description">Add new Mastodon Account</string>
|
||||
<string name="action_lists">Lists</string>
|
||||
<string name="title_lists">Lists</string>
|
||||
<string name="error_create_list">Could not create list</string>
|
||||
<string name="error_rename_list">Could not update list</string>
|
||||
<string name="error_delete_list">Could not delete list</string>
|
||||
<string name="title_lists_loading">Lists - loading…</string>
|
||||
<string name="title_lists_failed">Lists - failed to load</string>
|
||||
<string name="manage_lists">Manage lists</string>
|
||||
<string name="error_create_list_fmt">Could not create list \"%1$s\": %2$s</string>
|
||||
<string name="error_rename_list_fmt">Could not update list \"%1$s\": %2$s</string>
|
||||
<string name="error_delete_list_fmt">Could not delete list \"%1$s\": %2$s</string>
|
||||
<string name="action_create_list">Create a list</string>
|
||||
<string name="action_rename_list">Update the list</string>
|
||||
<string name="action_delete_list">Delete the list</string>
|
||||
|
@ -708,7 +710,6 @@
|
|||
<string name="janky_animation_title">You may need to restart your device</string>
|
||||
<string name="janky_animation_msg">This version of Pachli may trigger an Android bug on some devices, and show broken animations.\n\nFor example, when tapping a post to view a thread.\n\nIf you see this you will need to restart your device.\n\nYou only need to do this once.\n\nThis is Android bug, there is nothing Pachli can do.</string>
|
||||
|
||||
|
||||
<string name="server_repository_error">Could not fetch server info for %1$s: %2$s</string>
|
||||
<string name="server_repository_error_get_well_known_node_info">fetching /.well-known/nodeinfo failed: %1$s</string>
|
||||
<string name="server_repository_error_unsupported_schema">/.well-known/nodeinfo did not contain understandable schemas</string>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.data.di
|
||||
|
||||
import app.pachli.core.data.repository.ListsRepository
|
||||
import app.pachli.core.data.repository.NetworkListsRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
abstract class DataModule {
|
||||
@Binds
|
||||
internal abstract fun bindsListsRepository(
|
||||
listsRepository: NetworkListsRepository,
|
||||
): ListsRepository
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import app.pachli.core.network.model.MastoList
|
||||
import app.pachli.core.network.model.TimelineAccount
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiError
|
||||
import com.github.michaelbull.result.Result
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
sealed interface Lists {
|
||||
data object Loading : Lists
|
||||
data class Loaded(val lists: List<MastoList>) : Lists
|
||||
}
|
||||
|
||||
/** Marker for errors that include the ID of the list */
|
||||
interface HasListId {
|
||||
val listId: String
|
||||
}
|
||||
|
||||
/** Errors that can be returned from this repository */
|
||||
interface ListsError : ApiError {
|
||||
@JvmInline
|
||||
value class Create(private val error: ApiError) : ListsError, ApiError by error
|
||||
|
||||
@JvmInline
|
||||
value class Retrieve(private val error: ApiError) : ListsError, ApiError by error
|
||||
|
||||
@JvmInline
|
||||
value class Update(private val error: ApiError) : ListsError, ApiError by error
|
||||
|
||||
@JvmInline
|
||||
value class Delete(private val error: ApiError) : ListsError, ApiError by error
|
||||
|
||||
data class GetListsWithAccount(val accountId: String, private val error: ApiError) : ListsError, ApiError by error
|
||||
|
||||
data class GetAccounts(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, ApiError by error
|
||||
|
||||
data class DeleteAccounts(override val listId: String, private val error: ApiError) : ListsError, HasListId, ApiError by error
|
||||
}
|
||||
|
||||
interface ListsRepository {
|
||||
val lists: StateFlow<Result<Lists, ListsError.Retrieve>>
|
||||
|
||||
/** Make an API call to refresh [lists] */
|
||||
fun refresh()
|
||||
|
||||
/**
|
||||
* Create a new list
|
||||
*
|
||||
* @param title The new lists title
|
||||
* @param exclusive True if the list is exclusive
|
||||
* @return Details of the new list if successfuly, or an error
|
||||
*/
|
||||
suspend fun createList(title: String, exclusive: Boolean): Result<MastoList, ListsError.Create>
|
||||
|
||||
/**
|
||||
* Edit an existing list.
|
||||
*
|
||||
* @param listId ID of the list to edit
|
||||
* @param title New title of the list
|
||||
* @param exclusive New exclusive vale for the list
|
||||
* @return Amended list, or an error
|
||||
*/
|
||||
suspend fun editList(listId: String, title: String, exclusive: Boolean): Result<MastoList, ListsError.Update>
|
||||
|
||||
/**
|
||||
* Delete an existing list
|
||||
*
|
||||
* @param listId ID of the list to delete
|
||||
* @return A successful result, or an error
|
||||
*/
|
||||
suspend fun deleteList(listId: String): Result<Unit, ListsError.Delete>
|
||||
|
||||
/**
|
||||
* Fetch the lists with [accountId] as a member
|
||||
*
|
||||
* @param accountId ID of the account to search for
|
||||
* @result List of Mastodon lists the account is a member of, or an error
|
||||
*/
|
||||
suspend fun getListsWithAccount(accountId: String): Result<List<MastoList>, ListsError.GetListsWithAccount>
|
||||
|
||||
/**
|
||||
* Fetch the members of a list
|
||||
*
|
||||
* @param listId ID of the list to fetch membership for
|
||||
* @return List of [TimelineAccount] that are members of the list, or an error
|
||||
*/
|
||||
suspend fun getAccountsInList(listId: String): Result<List<TimelineAccount>, ListsError.GetAccounts>
|
||||
|
||||
/**
|
||||
* Add one or more accounts to a list
|
||||
*
|
||||
* @param listId ID of the list to add accounts to
|
||||
* @param accountIds IDs of the accounts to add
|
||||
* @return A successful result, or an error
|
||||
*/
|
||||
suspend fun addAccountsToList(listId: String, accountIds: List<String>): Result<Unit, ListsError.AddAccounts>
|
||||
|
||||
/**
|
||||
* Remove one or more accounts from a list
|
||||
*
|
||||
* @param listId ID of the list to remove accounts from
|
||||
* @param accountIds IDs of the accounts to remove
|
||||
* @return A successful result, or an error
|
||||
*/
|
||||
suspend fun deleteAccountsFromList(listId: String, accountIds: List<String>): Result<Unit, ListsError.DeleteAccounts>
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import app.pachli.core.accounts.AccountManager
|
||||
import app.pachli.core.common.di.ApplicationScope
|
||||
import app.pachli.core.data.repository.ListsError.Create
|
||||
import app.pachli.core.data.repository.ListsError.Delete
|
||||
import app.pachli.core.data.repository.ListsError.GetListsWithAccount
|
||||
import app.pachli.core.data.repository.ListsError.Retrieve
|
||||
import app.pachli.core.data.repository.ListsError.Update
|
||||
import app.pachli.core.network.model.MastoList
|
||||
import app.pachli.core.network.model.TimelineAccount
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.coroutines.binding.binding
|
||||
import com.github.michaelbull.result.mapBoth
|
||||
import com.github.michaelbull.result.mapError
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Singleton
|
||||
class NetworkListsRepository @Inject constructor(
|
||||
@ApplicationScope private val externalScope: CoroutineScope,
|
||||
private val api: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
) : ListsRepository {
|
||||
private val _lists = MutableStateFlow<Result<Lists, Retrieve>>(Ok(Lists.Loading))
|
||||
override val lists: StateFlow<Result<Lists, Retrieve>> get() = _lists.asStateFlow()
|
||||
|
||||
init {
|
||||
externalScope.launch { accountManager.activeAccountFlow.collect { refresh() } }
|
||||
}
|
||||
|
||||
override fun refresh() {
|
||||
externalScope.launch {
|
||||
_lists.value = Ok(Lists.Loading)
|
||||
_lists.value = api.getLists()
|
||||
.mapBoth(
|
||||
{ Ok(Lists.Loaded(it.body)) },
|
||||
{ Err(Retrieve(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createList(title: String, exclusive: Boolean): Result<MastoList, Create> = binding {
|
||||
externalScope.async {
|
||||
api.createList(title, exclusive).mapError { Create(it) }.bind().run {
|
||||
refresh()
|
||||
body
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
override suspend fun editList(listId: String, title: String, exclusive: Boolean): Result<MastoList, Update> = binding {
|
||||
externalScope.async {
|
||||
api.updateList(listId, title, exclusive).mapError { Update(it) }.bind().run {
|
||||
refresh()
|
||||
body
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
override suspend fun deleteList(listId: String): Result<Unit, Delete> = binding {
|
||||
externalScope.async {
|
||||
api.deleteList(listId).mapError { Delete(it) }.bind().run { refresh() }
|
||||
}.await()
|
||||
}
|
||||
|
||||
override suspend fun getListsWithAccount(accountId: String): Result<List<MastoList>, GetListsWithAccount> = binding {
|
||||
api.getListsIncludesAccount(accountId).mapError { GetListsWithAccount(accountId, it) }.bind().body
|
||||
}
|
||||
|
||||
override suspend fun getAccountsInList(listId: String): Result<List<TimelineAccount>, ListsError.GetAccounts> = binding {
|
||||
api.getAccountsInList(listId, 0).mapError { ListsError.GetAccounts(listId, it) }.bind().body
|
||||
}
|
||||
|
||||
override suspend fun addAccountsToList(listId: String, accountIds: List<String>): Result<Unit, ListsError.AddAccounts> = binding {
|
||||
externalScope.async {
|
||||
api.addAccountToList(listId, accountIds).mapError { ListsError.AddAccounts(listId, it) }.bind()
|
||||
}.await()
|
||||
}
|
||||
|
||||
override suspend fun deleteAccountsFromList(listId: String, accountIds: List<String>): Result<Unit, ListsError.DeleteAccounts> = binding {
|
||||
externalScope.async {
|
||||
api.deleteAccountFromList(listId, accountIds).mapError { ListsError.DeleteAccounts(listId, it) }.bind()
|
||||
}.await()
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ import app.pachli.core.network.json.Guarded
|
|||
import app.pachli.core.network.json.HasDefault
|
||||
import app.pachli.core.network.retrofit.InstanceSwitchAuthInterceptor
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiResultCallAdapterFactory
|
||||
import app.pachli.core.preferences.PrefKeys.HTTP_PROXY_ENABLED
|
||||
import app.pachli.core.preferences.PrefKeys.HTTP_PROXY_PORT
|
||||
import app.pachli.core.preferences.PrefKeys.HTTP_PROXY_SERVER
|
||||
|
@ -130,6 +131,7 @@ object NetworkModule {
|
|||
.client(httpClient)
|
||||
.addConverterFactory(EnumConstantConverterFactory)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResultCallAdapterFactory.create())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass
|
|||
data class MastoList(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val exclusive: Boolean?,
|
||||
/**
|
||||
* List's exclusivity (whether posts are hidden from the home timeline).
|
||||
* Null implies the server does not support this feature.
|
||||
*/
|
||||
val exclusive: Boolean? = null,
|
||||
)
|
||||
|
|
|
@ -49,6 +49,7 @@ import app.pachli.core.network.model.TimelineAccount
|
|||
import app.pachli.core.network.model.Translation
|
||||
import app.pachli.core.network.model.TrendingTag
|
||||
import app.pachli.core.network.model.TrendsLink
|
||||
import app.pachli.core.network.retrofit.apiresult.ApiResult
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
|
@ -361,7 +362,7 @@ interface MastodonApi {
|
|||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("following") following: Boolean? = null,
|
||||
): NetworkResult<List<TimelineAccount>>
|
||||
): ApiResult<List<TimelineAccount>>
|
||||
|
||||
@GET("api/v1/accounts/{id}")
|
||||
suspend fun account(
|
||||
|
@ -544,19 +545,19 @@ interface MastodonApi {
|
|||
): NetworkResult<Unit>
|
||||
|
||||
@GET("/api/v1/lists")
|
||||
suspend fun getLists(): NetworkResult<List<MastoList>>
|
||||
suspend fun getLists(): ApiResult<List<MastoList>>
|
||||
|
||||
@GET("/api/v1/accounts/{id}/lists")
|
||||
suspend fun getListsIncludesAccount(
|
||||
@Path("id") accountId: String,
|
||||
): NetworkResult<List<MastoList>>
|
||||
): ApiResult<List<MastoList>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists")
|
||||
suspend fun createList(
|
||||
@Field("title") title: String,
|
||||
@Field("exclusive") exclusive: Boolean?,
|
||||
): NetworkResult<MastoList>
|
||||
): ApiResult<MastoList>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/lists/{listId}")
|
||||
|
@ -564,18 +565,18 @@ interface MastodonApi {
|
|||
@Path("listId") listId: String,
|
||||
@Field("title") title: String,
|
||||
@Field("exclusive") exclusive: Boolean?,
|
||||
): NetworkResult<MastoList>
|
||||
): ApiResult<MastoList>
|
||||
|
||||
@DELETE("api/v1/lists/{listId}")
|
||||
suspend fun deleteList(
|
||||
@Path("listId") listId: String,
|
||||
): NetworkResult<Unit>
|
||||
): ApiResult<Unit>
|
||||
|
||||
@GET("api/v1/lists/{listId}/accounts")
|
||||
suspend fun getAccountsInList(
|
||||
@Path("listId") listId: String,
|
||||
@Query("limit") limit: Int,
|
||||
): NetworkResult<List<TimelineAccount>>
|
||||
): ApiResult<List<TimelineAccount>>
|
||||
|
||||
@FormUrlEncoded
|
||||
// @DELETE doesn't support fields
|
||||
|
@ -583,14 +584,14 @@ interface MastodonApi {
|
|||
suspend fun deleteAccountFromList(
|
||||
@Path("listId") listId: String,
|
||||
@Field("account_ids[]") accountIds: List<String>,
|
||||
): NetworkResult<Unit>
|
||||
): ApiResult<Unit>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists/{listId}/accounts")
|
||||
suspend fun addAccountToList(
|
||||
@Path("listId") listId: String,
|
||||
@Field("account_ids[]") accountIds: List<String>,
|
||||
): NetworkResult<Unit>
|
||||
): ApiResult<Unit>
|
||||
|
||||
@GET("/api/v1/conversations")
|
||||
suspend fun getConversations(
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Type
|
||||
import okhttp3.Headers
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
/**
|
||||
* Result monad modeling a response from the API.
|
||||
*/
|
||||
typealias ApiResult<T> = Result<ApiResponse<T>, ApiError>
|
||||
|
||||
/**
|
||||
* A successful response from an API call.
|
||||
*
|
||||
* @param headers The HTTP headers from the response
|
||||
* @param body The response body, converted to [T]
|
||||
*/
|
||||
data class ApiResponse<out T>(
|
||||
val headers: Headers,
|
||||
val body: T,
|
||||
)
|
||||
|
||||
/**
|
||||
* A failed response from an API call.
|
||||
*/
|
||||
interface ApiError {
|
||||
// This has to be Throwable, not Exception, because Retrofit exposes
|
||||
// errors as Throwable
|
||||
val throwable: Throwable
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
is JsonDataException -> JsonParse(exception)
|
||||
is IOException -> IO(exception)
|
||||
else -> Unknown(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Unknown(override val throwable: Throwable) : ApiError
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
/** 4xx errors */
|
||||
sealed interface ClientError : HttpError {
|
||||
companion object {
|
||||
fun from(exception: HttpException): ClientError {
|
||||
return when (exception.code()) {
|
||||
401 -> Unauthorized(exception)
|
||||
404 -> NotFound(exception)
|
||||
410 -> Gone(exception)
|
||||
else -> UnknownClientError(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** 5xx errors */
|
||||
sealed interface ServerError : HttpError {
|
||||
companion object {
|
||||
fun from(exception: HttpException): ServerError {
|
||||
return when (exception.code()) {
|
||||
500 -> Internal(exception)
|
||||
501 -> NotImplemented(exception)
|
||||
502 -> BadGateway(exception)
|
||||
503 -> ServiceUnavailable(exception)
|
||||
else -> UnknownServerError(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
data class JsonParse(override val throwable: JsonDataException) : ApiError
|
||||
sealed interface NetworkError : ApiError
|
||||
data class IO(override val throwable: Exception) : NetworkError
|
||||
|
||||
/**
|
||||
* Creates an [ApiResult] from a [Response].
|
||||
*/
|
||||
fun <T> Result.Companion.from(response: Response<T>, successType: Type): ApiResult<T> {
|
||||
if (!response.isSuccessful) {
|
||||
val err = ApiError.from(HttpException(response))
|
||||
return Err(err)
|
||||
}
|
||||
|
||||
// Skip body processing for successful responses expecting Unit
|
||||
if (successType == Unit::class.java) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return Ok(ApiResponse(response.headers(), Unit as T))
|
||||
}
|
||||
|
||||
response.body()?.let { body ->
|
||||
return Ok(ApiResponse(response.headers(), body))
|
||||
}
|
||||
|
||||
return Err(ApiError.from(HttpException(response)))
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import java.lang.reflect.Type
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
internal class ApiResultCall<T : Any>(
|
||||
private val delegate: Call<T>,
|
||||
private val successType: Type,
|
||||
) : Call<ApiResult<T>> {
|
||||
override fun enqueue(callback: Callback<ApiResult<T>>) = delegate.enqueue(
|
||||
object : Callback<T> {
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
callback.onResponse(
|
||||
this@ApiResultCall,
|
||||
Response.success(ApiResult.from(response, successType)),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, throwable: Throwable) {
|
||||
val err: ApiResult<T> = Err(ApiError.from(throwable))
|
||||
|
||||
callback.onResponse(this@ApiResultCall, Response.success(err))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun isExecuted() = delegate.isExecuted
|
||||
|
||||
override fun clone() = ApiResultCall(delegate.clone(), successType)
|
||||
|
||||
override fun isCanceled() = delegate.isCanceled
|
||||
|
||||
override fun cancel() = delegate.cancel()
|
||||
|
||||
override fun execute(): Response<ApiResult<T>> {
|
||||
throw UnsupportedOperationException("ApiResultCall doesn't support synchronized execution")
|
||||
}
|
||||
|
||||
override fun request(): Request = delegate.request()
|
||||
|
||||
override fun timeout(): Timeout = delegate.timeout()
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import java.lang.reflect.Type
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
|
||||
internal class ApiResultCallAdapter<R : Any>(
|
||||
private val successType: Type,
|
||||
) : CallAdapter<R, Call<ApiResult<R>>> {
|
||||
override fun responseType(): Type = successType
|
||||
|
||||
override fun adapt(call: Call<R>): Call<ApiResult<R>> {
|
||||
return ApiResultCall(call, successType)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import com.github.michaelbull.result.Result
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Retrofit
|
||||
|
||||
/**
|
||||
* Call adapter factory for `Result<ApiResponse<T>, ApiError>` (aliased to `ApiResult<T>`).
|
||||
*/
|
||||
class ApiResultCallAdapterFactory internal constructor() : CallAdapter.Factory() {
|
||||
override fun get(
|
||||
returnType: Type,
|
||||
annotations: Array<Annotation>,
|
||||
retrofit: Retrofit,
|
||||
): CallAdapter<*, *>? {
|
||||
// Check the expected return type:
|
||||
//
|
||||
// - In suspend calls this is a `retrofit2.Call"
|
||||
// - In non-suspend calls this is `Result<V, E>`
|
||||
val rawReturnType = getRawType(returnType)
|
||||
if (rawReturnType != Call::class.java && rawReturnType != Result::class.java) {
|
||||
return null
|
||||
}
|
||||
|
||||
// The return type must be parameterised with the expected response body type
|
||||
check(returnType is ParameterizedType) {
|
||||
"return type must be parameterized as Call<ApiResult<Foo>>, Call<ApiResult<out Foo>>, " +
|
||||
"ApiResult<Foo> or ApiResult<out Foo>"
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the entire response, as seen by Retrofit. Expected to
|
||||
* be `Result<ApiResponse<T>, ApiError>`.
|
||||
*/
|
||||
val responseType = getParameterUpperBound(0, returnType)
|
||||
|
||||
// If rawReturnType is `Result` then this is a non-suspending call (synchronous)
|
||||
// so delegate to SyncApiResultCallAdapter
|
||||
if (rawReturnType == Result::class.java) {
|
||||
check(responseType is ParameterizedType) {
|
||||
"Response must be parameterized as ApiResult<Foo> or ApiResult<out Foo>"
|
||||
}
|
||||
val successBodyType = getParameterUpperBound(0, responseType)
|
||||
return SyncApiResultCallAdapter<Any>(successBodyType)
|
||||
}
|
||||
|
||||
// If the response type is not Result then we can't handle this type
|
||||
if (getRawType(responseType) != Result::class.java) return null
|
||||
|
||||
check(responseType is ParameterizedType) {
|
||||
"Response must be parameterized as ApiResult<Foo> or ApiResult<out Foo>"
|
||||
}
|
||||
|
||||
// Ensure the V in Result<V, E> is ApiResponse<T>
|
||||
val successType = getParameterUpperBound(0, responseType)
|
||||
check(successType is ParameterizedType) { "Success type must be parameterized" }
|
||||
if (getRawType(successType) != ApiResponse::class.java) return null
|
||||
|
||||
// Fetch the type T from ApiResult<T>
|
||||
val successBodyType = getParameterUpperBound(0, successType)
|
||||
|
||||
return ApiResultCallAdapter<Any>(successBodyType)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(): ApiResultCallAdapterFactory = ApiResultCallAdapterFactory()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import java.lang.reflect.Type
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
|
||||
/**
|
||||
* Retrofit call adapters for non-suspending functions that return
|
||||
* `Result<ApiResponse<V>, ApiError>`.
|
||||
*
|
||||
* @param successType The type of the expected successful result (i.e,.
|
||||
* the `V` in `Result<ApiResponse<V>, ApiError>`)
|
||||
*/
|
||||
internal class SyncApiResultCallAdapter<T : Any>(
|
||||
private val successType: Type,
|
||||
) : CallAdapter<T, ApiResult<T>> {
|
||||
override fun responseType(): Type = successType
|
||||
|
||||
override fun adapt(call: Call<T>): ApiResult<T> {
|
||||
return try {
|
||||
ApiResult.from(call.execute(), successType)
|
||||
} catch (e: Exception) {
|
||||
Err(ApiError.from(e))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package app.pachli.core.network.retrofit.apiresult
|
||||
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlin.reflect.javaType
|
||||
import kotlin.reflect.typeOf
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import retrofit2.Call
|
||||
import retrofit2.Retrofit
|
||||
|
||||
class ApiResultCallAdapterFactoryTest {
|
||||
private val retrofit = Retrofit.Builder().baseUrl("http://example.com").build()
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Test
|
||||
fun `should return an ApiResultCallAdapter`() {
|
||||
val callType = typeOf<Call<ApiResult<Site>>>()
|
||||
val adapter = ApiResultCallAdapterFactory().get(callType.javaType, arrayOf(), retrofit)
|
||||
assertThat(adapter).isInstanceOf(ApiResultCallAdapter::class.java)
|
||||
assertThat(adapter?.responseType()).isEqualTo(Site::class.java)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Test
|
||||
fun `should return a SyncApiResultCallAdapter`() {
|
||||
val responseType = typeOf<ApiResult<Site>>()
|
||||
val adapter = ApiResultCallAdapterFactory().get(responseType.javaType, arrayOf(), retrofit)
|
||||
|
||||
assertThat(adapter).isInstanceOf(SyncApiResultCallAdapter::class.java)
|
||||
assertThat(adapter?.responseType()).isEqualTo(Site::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw error if the type is not parameterized`() {
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
ApiResultCallAdapterFactory().get(Result::class.java, arrayOf(), retrofit)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return null if the type is not supported`() {
|
||||
val adapter = ApiResultCallAdapterFactory().get(Site::class.java, arrayOf(), retrofit)
|
||||
|
||||
assertThat(adapter).isNull()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.getError
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import java.io.IOException
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
class ApiResultCallTest {
|
||||
private val backingCall = TestCall<String>()
|
||||
private val networkApiResultCall = ApiResultCall(backingCall, String::class.java)
|
||||
|
||||
@Test
|
||||
fun `should throw an error when invoking 'execute'`() {
|
||||
assertThrows(UnsupportedOperationException::class.java) {
|
||||
networkApiResultCall.execute()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should delegate properties to backing call`() {
|
||||
with(networkApiResultCall) {
|
||||
assertThat(isExecuted).isEqualTo(backingCall.isExecuted)
|
||||
assertThat(isCanceled).isEqualTo(backingCall.isCanceled)
|
||||
assertThat(request()).isEqualTo(backingCall.request())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return new instance when cloned`() {
|
||||
val clonedCall = networkApiResultCall.clone()
|
||||
assert(clonedCall !== networkApiResultCall)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should cancel backing call as well when cancelled`() {
|
||||
networkApiResultCall.cancel()
|
||||
assert(backingCall.isCanceled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should parse successful call as ApiResult-success`() {
|
||||
val okResponse = Response.success("Test body")
|
||||
|
||||
networkApiResultCall.enqueue(
|
||||
object : Callback<ApiResult<String>> {
|
||||
override fun onResponse(call: Call<ApiResult<String>>, response: Response<ApiResult<String>>) {
|
||||
assertThat(response.isSuccessful).isTrue()
|
||||
assertThat(response.body()).isEqualTo(ApiResult.from(okResponse, String::class.java))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<ApiResult<String>>, t: Throwable) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
},
|
||||
)
|
||||
backingCall.complete(okResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should parse call with 404 error code as ApiResult-failure`() {
|
||||
val errorResponse = Response.error<String>(404, "not found".toResponseBody())
|
||||
|
||||
networkApiResultCall.enqueue(
|
||||
object : Callback<ApiResult<String>> {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<ApiResult<String>>, t: Throwable) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
backingCall.complete(errorResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should parse call with IOException as ApiResult-failure`() {
|
||||
val error = Err(IO(IOException()))
|
||||
|
||||
networkApiResultCall.enqueue(
|
||||
object : Callback<ApiResult<String>> {
|
||||
override fun onResponse(call: Call<ApiResult<String>>, response: Response<ApiResult<String>>) {
|
||||
assertThat(response.body()).isEqualTo(error)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<ApiResult<String>>, t: Throwable) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
backingCall.completeWithException(error.error.throwable)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import com.github.michaelbull.result.get
|
||||
import com.github.michaelbull.result.getError
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.JsonEncodingException
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.SocketPolicy
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
|
||||
class ApiTest {
|
||||
private var mockWebServer = MockWebServer()
|
||||
|
||||
private lateinit var api: TestApi
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockWebServer.start()
|
||||
|
||||
val moshi = Moshi.Builder().build()
|
||||
|
||||
api = Retrofit.Builder()
|
||||
.baseUrl(mockWebServer.url("/"))
|
||||
.addCallAdapterFactory(ApiResultCallAdapterFactory())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(OkHttpClient())
|
||||
.build()
|
||||
.create(TestApi::class.java)
|
||||
}
|
||||
|
||||
@After
|
||||
fun shutdown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
private fun mockResponse(responseCode: Int, body: String = "") = MockResponse()
|
||||
.setResponseCode(responseCode)
|
||||
.setBody(body)
|
||||
|
||||
@Test
|
||||
fun `suspending call - should return the correct test object`() = runTest {
|
||||
val response =
|
||||
mockResponse(
|
||||
200,
|
||||
"""
|
||||
{
|
||||
"url": "https://pachli.app",
|
||||
"title": "Pachli"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
mockWebServer.enqueue(response)
|
||||
val responseObject = api.getSiteAsync().get()?.body
|
||||
|
||||
assertThat(responseObject).isEqualTo(Site("https://pachli.app", "Pachli"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking call - should return the correct test object`() {
|
||||
val response =
|
||||
mockResponse(
|
||||
200,
|
||||
"""
|
||||
{
|
||||
"url": "https://pachli.app",
|
||||
"title": "Pachli"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
mockWebServer.enqueue(response)
|
||||
val responseObject = api.getSiteSync().get()?.body
|
||||
|
||||
assertThat(responseObject).isEqualTo(Site("https://pachli.app", "Pachli"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suspending call - returns complex objects`() = runTest {
|
||||
val response = mockResponse(
|
||||
200,
|
||||
"""
|
||||
[
|
||||
{
|
||||
"url": "https://pachli.app",
|
||||
"title": "Pachli"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/pachli/pachli-android",
|
||||
"title": "GitHub - Pachli"
|
||||
}
|
||||
]
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
mockWebServer.enqueue(response)
|
||||
val responseObject = api.getSitesAsync().get()?.body
|
||||
|
||||
assertThat(responseObject).isEqualTo(
|
||||
listOf(
|
||||
Site("https://pachli.app", "Pachli"),
|
||||
Site("https://github.com/pachli/pachli-android", "GitHub - Pachli"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suspending call - should return an Internal error when the server returns error 500`() = runTest {
|
||||
val errorCode = 500
|
||||
val response = mockResponse(errorCode)
|
||||
|
||||
mockWebServer.enqueue(response)
|
||||
val responseObject = api.getSiteAsync().getError()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking call - should return an Internal error when the server returns error 500`() {
|
||||
val errorCode = 500
|
||||
val response = mockResponse(errorCode)
|
||||
|
||||
mockWebServer.enqueue(response)
|
||||
|
||||
val responseObject = api.getSiteSync().getError()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suspending call - should return an IO error when the network fails`() {
|
||||
mockWebServer.enqueue(MockResponse().apply { socketPolicy = SocketPolicy.DISCONNECT_AFTER_REQUEST })
|
||||
val responseObject =
|
||||
runBlocking {
|
||||
api.getSiteAsync()
|
||||
}
|
||||
|
||||
val error = responseObject.getError() as? IO
|
||||
|
||||
assertThat(error).isInstanceOf(IO::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking call - should return an IO error when the network fails`() {
|
||||
mockWebServer.enqueue(MockResponse().apply { socketPolicy = SocketPolicy.DISCONNECT_AFTER_REQUEST })
|
||||
val responseObject = runBlocking { api.getSiteSync() }
|
||||
|
||||
val error = responseObject.getError() as? IO
|
||||
|
||||
assertThat(error).isInstanceOf(IO::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suspending call - should return a JsonParse error on incorrect JSON data`() {
|
||||
val response = mockResponse(200, """{"wrong": "shape"}""")
|
||||
|
||||
mockWebServer.enqueue(response)
|
||||
val responseObject = api.getSitesAsync().getError()
|
||||
|
||||
val error = responseObject as? JsonParse
|
||||
|
||||
assertThat(error).isInstanceOf(JsonParse::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suspending call - should return an IO(JsonEncodingException) error on invalid JSON`() {
|
||||
val response = mockResponse(200, "not even JSON")
|
||||
mockWebServer.enqueue(response)
|
||||
|
||||
val responseObject = api.getSitesAsync().getError()
|
||||
|
||||
val error = responseObject as? IO
|
||||
|
||||
// Moshi reports invalid JSON as an IoException wrapping a JsonEncodingException
|
||||
assertThat(error).isInstanceOf(IO::class.java)
|
||||
assertThat(error?.throwable).isInstanceOf(JsonEncodingException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suspending call - should pass through non-Result types`() = runTest {
|
||||
val response = mockResponse(200)
|
||||
mockWebServer.enqueue(response)
|
||||
|
||||
val responseObject = api.getResponseAsync()
|
||||
|
||||
assertThat(responseObject.code()).isEqualTo(200)
|
||||
assertThat(responseObject.body()).isInstanceOf(Unit::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking call - should pass through non-Result types`() {
|
||||
val response = mockResponse(200)
|
||||
mockWebServer.enqueue(response)
|
||||
|
||||
val responseObject = api.getResponseSync().execute()
|
||||
|
||||
assertThat(responseObject.code()).isEqualTo(200)
|
||||
assertThat(responseObject.body()).isInstanceOf(Unit::class.java)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
Tests for ApiResultCallAdapterFactory and related methods heavily draw on
|
||||
https://github.com/connyduck/networkresult-calladapter/tree/main/calladapter/src/test/kotlin/at/connyduck/calladapter/networkresult
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
|
||||
/**
|
||||
* Sample data class to use while testing. Models a database of
|
||||
* sites, with a URL and a title.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Site(
|
||||
val url: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
interface TestApi {
|
||||
@GET("/site")
|
||||
suspend fun getSiteAsync(): ApiResult<Site>
|
||||
|
||||
@GET("/site")
|
||||
fun getSiteSync(): ApiResult<Site>
|
||||
|
||||
@GET("/sites")
|
||||
fun getSitesAsync(): ApiResult<List<Site>>
|
||||
|
||||
@GET("/response")
|
||||
suspend fun getResponseAsync(): Response<Unit>
|
||||
|
||||
@GET("/response")
|
||||
fun getResponseSync(): Call<Unit>
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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.network.retrofit.apiresult
|
||||
|
||||
import java.io.InterruptedIOException
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class TestCall<T> : Call<T> {
|
||||
private var executed = false
|
||||
private var canceled = false
|
||||
private var callback: Callback<T>? = null
|
||||
private var request = Request.Builder().url("http://example.com").build()
|
||||
|
||||
fun completeWithException(t: Throwable) {
|
||||
synchronized(this) {
|
||||
callback?.onFailure(this, t)
|
||||
}
|
||||
}
|
||||
|
||||
fun complete(body: T): Unit = complete(Response.success(body))
|
||||
|
||||
fun complete(response: Response<T>) {
|
||||
synchronized(this) {
|
||||
callback?.onResponse(this, response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun enqueue(callback: Callback<T>) {
|
||||
synchronized(this) {
|
||||
this.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
override fun isExecuted(): Boolean = synchronized(this) { executed }
|
||||
|
||||
override fun isCanceled(): Boolean = synchronized(this) { canceled }
|
||||
|
||||
override fun clone(): TestCall<T> = TestCall()
|
||||
|
||||
override fun cancel() {
|
||||
synchronized(this) {
|
||||
if (canceled) return
|
||||
canceled = true
|
||||
|
||||
val exception = InterruptedIOException("canceled")
|
||||
callback?.onFailure(this, exception)
|
||||
}
|
||||
}
|
||||
|
||||
override fun execute(): Response<T> {
|
||||
throw UnsupportedOperationException("TestCall does not support synchronous execution")
|
||||
}
|
||||
|
||||
override fun request(): Request = request
|
||||
|
||||
override fun timeout(): Timeout = Timeout()
|
||||
}
|
Loading…
Reference in New Issue