refactor domain blocks to paging (#3801)
Adds Paging, ViewModel, error snackbars, removes Rx, renames everything to "domain blocks" to match the Masto Api part of #2992
This commit is contained in:
commit
84969586c8
|
@ -872,7 +872,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="777"
|
line="780"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -1928,7 +1928,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="450"
|
line="453"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2071,7 +2071,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="615"
|
line="618"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2082,7 +2082,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="617"
|
line="620"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2093,7 +2093,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="618"
|
line="621"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2104,7 +2104,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="621"
|
line="624"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2115,7 +2115,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="666"
|
line="669"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2126,7 +2126,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="715"
|
line="718"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2137,7 +2137,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="721"
|
line="724"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2148,7 +2148,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="762"
|
line="765"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2159,7 +2159,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="798"
|
line="801"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -2734,17 +2734,7 @@
|
||||||
line="202"
|
line="202"
|
||||||
column="21"/>
|
column="21"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="SyntheticAccessor"
|
|
||||||
message="Access to `private` method `fetchInstances` of class `InstanceListFragment` requires synthetic accessor"
|
|
||||||
errorLine1=" fetchInstances(bottomId)"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt"
|
|
||||||
line="56"
|
line="56"
|
||||||
column="21"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="SyntheticAccessor"
|
id="SyntheticAccessor"
|
||||||
|
@ -4242,13 +4232,6 @@
|
||||||
column="54"/>
|
column="54"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ConvertToWebp"
|
|
||||||
message="One or more images in this project can be converted to the WebP format which typically results in smaller file sizes, even for lossless conversion">
|
|
||||||
<location
|
|
||||||
file="src/blue/res/mipmap-xxxhdpi/ic_launcher.png"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="SelectableText"
|
id="SelectableText"
|
||||||
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
||||||
|
@ -4568,6 +4551,17 @@
|
||||||
column="2"/>
|
column="2"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="SelectableText"
|
||||||
|
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
||||||
|
errorLine1=" <TextView"
|
||||||
|
errorLine2=" ~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/res/layout/item_blocked_domain.xml"
|
||||||
|
line="27"
|
||||||
|
column="6"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="SelectableText"
|
id="SelectableText"
|
||||||
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
||||||
|
@ -4777,17 +4771,6 @@
|
||||||
column="6"/>
|
column="6"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="SelectableText"
|
|
||||||
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
|
||||||
errorLine1=" <TextView"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/item_muted_domain.xml"
|
|
||||||
line="27"
|
|
||||||
column="6"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="SelectableText"
|
id="SelectableText"
|
||||||
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
message="Consider making the text value selectable by specifying `android:textIsSelectable="true"`"
|
||||||
|
|
|
@ -148,7 +148,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.report.ReportActivity"
|
android:name=".components.report.ReportActivity"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
<activity android:name=".components.domainblocks.DomainBlocksActivity" />
|
||||||
<activity android:name=".components.scheduled.ScheduledStatusActivity" />
|
<activity android:name=".components.scheduled.ScheduledStatusActivity" />
|
||||||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||||
<activity android:name=".components.drafts.DraftsActivity" />
|
<activity android:name=".components.drafts.DraftsActivity" />
|
||||||
|
|
|
@ -61,7 +61,6 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AccountListFragment :
|
class AccountListFragment :
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
package com.keylesspalace.tusky.components.instancemute
|
package com.keylesspalace.tusky.components.domainblocks
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
|
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class InstanceListActivity : BaseActivity(), HasAndroidInjector {
|
class DomainBlocksActivity : BaseActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
||||||
|
@ -28,7 +27,7 @@ class InstanceListActivity : BaseActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
supportFragmentManager
|
supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(R.id.fragment_container, InstanceListFragment())
|
.replace(R.id.fragment_container, DomainBlocksFragment())
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.keylesspalace.tusky.components.domainblocks
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import com.keylesspalace.tusky.components.followedtags.FollowedTagsAdapter.Companion.STRING_COMPARATOR
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemBlockedDomainBinding
|
||||||
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
|
||||||
|
class DomainBlocksAdapter(
|
||||||
|
private val onUnmute: (String) -> Unit
|
||||||
|
) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemBlockedDomainBinding> {
|
||||||
|
val binding = ItemBlockedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return BindingHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingHolder<ItemBlockedDomainBinding>, position: Int) {
|
||||||
|
getItem(position)?.let { instance ->
|
||||||
|
holder.binding.blockedDomain.text = instance
|
||||||
|
holder.binding.blockedDomainUnblock.setOnClickListener {
|
||||||
|
onUnmute(instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package com.keylesspalace.tusky.components.domainblocks
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.FragmentDomainBlocksBinding
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectable {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
private val binding by viewBinding(FragmentDomainBlocksBinding::bind)
|
||||||
|
|
||||||
|
private val viewModel: DomainBlocksViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val adapter = DomainBlocksAdapter(viewModel::unblock)
|
||||||
|
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.uiEvents.collect { event ->
|
||||||
|
showSnackbar(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.domainPager.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.addLoadStateListener { loadState ->
|
||||||
|
binding.progressBar.visible(loadState.refresh == LoadState.Loading && adapter.itemCount == 0)
|
||||||
|
|
||||||
|
if (loadState.refresh is LoadState.Error) {
|
||||||
|
binding.recyclerView.hide()
|
||||||
|
binding.messageView.show()
|
||||||
|
val errorState = loadState.refresh as LoadState.Error
|
||||||
|
binding.messageView.setup(errorState.error) { adapter.retry() }
|
||||||
|
Log.w(TAG, "error loading blocked domains", errorState.error)
|
||||||
|
} else if (loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0) {
|
||||||
|
binding.recyclerView.hide()
|
||||||
|
binding.messageView.show()
|
||||||
|
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
|
||||||
|
} else {
|
||||||
|
binding.recyclerView.show()
|
||||||
|
binding.messageView.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnackbar(event: SnackbarEvent) {
|
||||||
|
val message = if (event.throwable == null) {
|
||||||
|
getString(event.message, event.domain)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, event.throwable)
|
||||||
|
val error = event.throwable.localizedMessage ?: getString(R.string.ui_error_unknown)
|
||||||
|
getString(event.message, event.domain, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.make(binding.recyclerView, message, Snackbar.LENGTH_LONG)
|
||||||
|
.setTextMaxLines(5)
|
||||||
|
.setAction(event.actionText, event.action)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "DomainBlocksFragment"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.keylesspalace.tusky.components.domainblocks
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
|
||||||
|
class DomainBlocksPagingSource(
|
||||||
|
private val domains: List<String>,
|
||||||
|
private val nextKey: String?
|
||||||
|
) : PagingSource<String, String>() {
|
||||||
|
override fun getRefreshKey(state: PagingState<String, String>): String? = null
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, String> {
|
||||||
|
return if (params is LoadParams.Refresh) {
|
||||||
|
LoadResult.Page(domains, null, nextKey)
|
||||||
|
} else {
|
||||||
|
LoadResult.Page(emptyList(), null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.keylesspalace.tusky.components.domainblocks
|
||||||
|
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadType
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.RemoteMediator
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
class DomainBlocksRemoteMediator(
|
||||||
|
private val api: MastodonApi,
|
||||||
|
private val repository: DomainBlocksRepository
|
||||||
|
) : RemoteMediator<String, String>() {
|
||||||
|
|
||||||
|
override suspend fun load(
|
||||||
|
loadType: LoadType,
|
||||||
|
state: PagingState<String, String>
|
||||||
|
): MediatorResult {
|
||||||
|
return try {
|
||||||
|
val response = request(loadType)
|
||||||
|
?: return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
|
||||||
|
return applyResponse(response)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun request(loadType: LoadType): Response<List<String>>? {
|
||||||
|
return when (loadType) {
|
||||||
|
LoadType.PREPEND -> null
|
||||||
|
LoadType.APPEND -> api.domainBlocks(maxId = repository.nextKey)
|
||||||
|
LoadType.REFRESH -> {
|
||||||
|
repository.nextKey = null
|
||||||
|
repository.domains.clear()
|
||||||
|
api.domainBlocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyResponse(response: Response<List<String>>): MediatorResult {
|
||||||
|
val tags = response.body()
|
||||||
|
if (!response.isSuccessful || tags == null) {
|
||||||
|
return MediatorResult.Error(HttpException(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
val links = HttpHeaderLink.parse(response.headers()["Link"])
|
||||||
|
repository.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id")
|
||||||
|
repository.domains.addAll(tags)
|
||||||
|
repository.invalidate()
|
||||||
|
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = repository.nextKey == null)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Tusky 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 Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.domainblocks
|
||||||
|
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.InvalidatingPagingSourceFactory
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
|
import at.connyduck.calladapter.networkresult.onSuccess
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DomainBlocksRepository @Inject constructor(
|
||||||
|
private val api: MastodonApi
|
||||||
|
) {
|
||||||
|
val domains: MutableList<String> = mutableListOf()
|
||||||
|
var nextKey: String? = null
|
||||||
|
|
||||||
|
private var factory = InvalidatingPagingSourceFactory {
|
||||||
|
DomainBlocksPagingSource(domains.toList(), nextKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
val domainPager = Pager(
|
||||||
|
config = PagingConfig(pageSize = PAGE_SIZE, initialLoadSize = PAGE_SIZE),
|
||||||
|
remoteMediator = DomainBlocksRemoteMediator(api, this),
|
||||||
|
pagingSourceFactory = factory
|
||||||
|
).flow
|
||||||
|
|
||||||
|
/** Invalidate the active paging source, see [PagingSource.invalidate] */
|
||||||
|
fun invalidate() {
|
||||||
|
factory.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun block(domain: String): NetworkResult<Unit> {
|
||||||
|
return api.blockDomain(domain).onSuccess {
|
||||||
|
domains.add(domain)
|
||||||
|
factory.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unblock(domain: String): NetworkResult<Unit> {
|
||||||
|
return api.unblockDomain(domain).onSuccess {
|
||||||
|
domains.remove(domain)
|
||||||
|
factory.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PAGE_SIZE = 20
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.keylesspalace.tusky.components.domainblocks
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
import at.connyduck.calladapter.networkresult.onFailure
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DomainBlocksViewModel @Inject constructor(
|
||||||
|
private val repo: DomainBlocksRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val domainPager = repo.domainPager.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
val uiEvents = MutableSharedFlow<SnackbarEvent>()
|
||||||
|
|
||||||
|
fun block(domain: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
repo.block(domain).onFailure { e ->
|
||||||
|
uiEvents.emit(
|
||||||
|
SnackbarEvent(
|
||||||
|
message = R.string.error_blocking_domain,
|
||||||
|
domain = domain,
|
||||||
|
throwable = e,
|
||||||
|
actionText = R.string.action_retry,
|
||||||
|
action = { block(domain) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unblock(domain: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
repo.unblock(domain).fold({
|
||||||
|
uiEvents.emit(
|
||||||
|
SnackbarEvent(
|
||||||
|
message = R.string.confirmation_domain_unmuted,
|
||||||
|
domain = domain,
|
||||||
|
throwable = null,
|
||||||
|
actionText = R.string.action_undo,
|
||||||
|
action = { block(domain) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, { e ->
|
||||||
|
uiEvents.emit(
|
||||||
|
SnackbarEvent(
|
||||||
|
message = R.string.error_unblocking_domain,
|
||||||
|
domain = domain,
|
||||||
|
throwable = e,
|
||||||
|
actionText = R.string.action_retry,
|
||||||
|
action = { unblock(domain) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnackbarEvent(
|
||||||
|
@StringRes val message: Int,
|
||||||
|
val domain: String,
|
||||||
|
val throwable: Throwable?,
|
||||||
|
@StringRes val actionText: Int,
|
||||||
|
val action: (View) -> Unit
|
||||||
|
)
|
|
@ -1,56 +0,0 @@
|
||||||
package com.keylesspalace.tusky.components.instancemute.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemMutedDomainBinding
|
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
|
||||||
|
|
||||||
class DomainMutesAdapter(
|
|
||||||
private val actionListener: InstanceActionListener
|
|
||||||
) : RecyclerView.Adapter<BindingHolder<ItemMutedDomainBinding>>() {
|
|
||||||
|
|
||||||
var instances: MutableList<String> = mutableListOf()
|
|
||||||
var bottomLoading: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemMutedDomainBinding> {
|
|
||||||
val binding = ItemMutedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
return BindingHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemMutedDomainBinding>, position: Int) {
|
|
||||||
val instance = instances[position]
|
|
||||||
|
|
||||||
holder.binding.mutedDomain.text = instance
|
|
||||||
holder.binding.mutedDomainUnmute.setOnClickListener {
|
|
||||||
actionListener.mute(false, instance, holder.bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
var count = instances.size
|
|
||||||
if (bottomLoading) {
|
|
||||||
++count
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addItems(newInstances: List<String>) {
|
|
||||||
val end = instances.size
|
|
||||||
instances.addAll(newInstances)
|
|
||||||
notifyItemRangeInserted(end, instances.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addItem(instance: String) {
|
|
||||||
instances.add(instance)
|
|
||||||
notifyItemInserted(instances.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeItem(position: Int) {
|
|
||||||
if (position >= 0 && position < instances.size) {
|
|
||||||
instances.removeAt(position)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
package com.keylesspalace.tusky.components.instancemute.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
|
||||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
|
||||||
import autodispose2.autoDispose
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.keylesspalace.tusky.R
|
|
||||||
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
|
|
||||||
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
|
|
||||||
import com.keylesspalace.tusky.databinding.FragmentInstanceListBinding
|
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
|
||||||
import com.keylesspalace.tusky.util.HttpHeaderLink
|
|
||||||
import com.keylesspalace.tusky.util.hide
|
|
||||||
import com.keylesspalace.tusky.util.show
|
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class InstanceListFragment : Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var api: MastodonApi
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentInstanceListBinding::bind)
|
|
||||||
|
|
||||||
private var fetching = false
|
|
||||||
private var bottomId: String? = null
|
|
||||||
private var adapter = DomainMutesAdapter(this)
|
|
||||||
private lateinit var scrollListener: EndlessOnScrollListener
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
|
|
||||||
val layoutManager = LinearLayoutManager(view.context)
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
|
|
||||||
scrollListener = object : EndlessOnScrollListener(layoutManager) {
|
|
||||||
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
|
|
||||||
if (bottomId != null) {
|
|
||||||
fetchInstances(bottomId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerView.addOnScrollListener(scrollListener)
|
|
||||||
fetchInstances()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mute(mute: Boolean, instance: String, position: Int) {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
if (mute) {
|
|
||||||
api.blockDomain(instance).fold({
|
|
||||||
adapter.addItem(instance)
|
|
||||||
}, { e ->
|
|
||||||
Log.e(TAG, "Error muting domain $instance", e)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
api.unblockDomain(instance).fold({
|
|
||||||
adapter.removeItem(position)
|
|
||||||
Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.action_undo) {
|
|
||||||
mute(true, instance, position)
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}, { e ->
|
|
||||||
Log.e(TAG, "Error unmuting domain $instance", e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchInstances(id: String? = null) {
|
|
||||||
if (fetching) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetching = true
|
|
||||||
binding.instanceProgressBar.show()
|
|
||||||
|
|
||||||
if (id != null) {
|
|
||||||
binding.recyclerView.post { adapter.bottomLoading = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
api.domainBlocks(id, bottomId)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
|
||||||
.subscribe(
|
|
||||||
{ response ->
|
|
||||||
val instances = response.body()
|
|
||||||
|
|
||||||
if (response.isSuccessful && instances != null) {
|
|
||||||
onFetchInstancesSuccess(instances, response.headers()["Link"])
|
|
||||||
} else {
|
|
||||||
onFetchInstancesFailure(Exception(response.message()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ throwable ->
|
|
||||||
onFetchInstancesFailure(throwable)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
|
|
||||||
adapter.bottomLoading = false
|
|
||||||
binding.instanceProgressBar.hide()
|
|
||||||
|
|
||||||
val links = HttpHeaderLink.parse(linkHeader)
|
|
||||||
val next = HttpHeaderLink.findByRelationType(links, "next")
|
|
||||||
val fromId = next?.uri?.getQueryParameter("max_id")
|
|
||||||
adapter.addItems(instances)
|
|
||||||
bottomId = fromId
|
|
||||||
fetching = false
|
|
||||||
|
|
||||||
if (adapter.itemCount == 0) {
|
|
||||||
binding.messageView.show()
|
|
||||||
binding.messageView.setup(
|
|
||||||
R.drawable.elephant_friend_empty,
|
|
||||||
R.string.message_empty,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
binding.messageView.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFetchInstancesFailure(throwable: Throwable) {
|
|
||||||
fetching = false
|
|
||||||
binding.instanceProgressBar.hide()
|
|
||||||
Log.e(TAG, "Fetch failure", throwable)
|
|
||||||
|
|
||||||
if (adapter.itemCount == 0) {
|
|
||||||
binding.messageView.show()
|
|
||||||
binding.messageView.setup(throwable) {
|
|
||||||
binding.messageView.hide()
|
|
||||||
this.fetchInstances(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "InstanceList" // logging tag
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package com.keylesspalace.tusky.components.instancemute.interfaces
|
|
||||||
|
|
||||||
interface InstanceActionListener {
|
|
||||||
fun mute(mute: Boolean, instance: String, position: Int)
|
|
||||||
}
|
|
|
@ -30,9 +30,9 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
||||||
|
import com.keylesspalace.tusky.components.domainblocks.DomainBlocksActivity
|
||||||
import com.keylesspalace.tusky.components.filters.FiltersActivity
|
import com.keylesspalace.tusky.components.filters.FiltersActivity
|
||||||
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
|
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
|
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
@ -156,7 +156,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
setTitle(R.string.title_domain_mutes)
|
setTitle(R.string.title_domain_mutes)
|
||||||
setIcon(R.drawable.ic_mute_24dp)
|
setIcon(R.drawable.ic_mute_24dp)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = Intent(context, InstanceListActivity::class.java)
|
val intent = Intent(context, DomainBlocksActivity::class.java)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivity(intent)
|
||||||
activity?.overridePendingTransition(
|
activity?.overridePendingTransition(
|
||||||
R.anim.slide_from_right,
|
R.anim.slide_from_right,
|
||||||
|
|
|
@ -29,11 +29,11 @@ import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
import com.keylesspalace.tusky.components.domainblocks.DomainBlocksActivity
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.filters.EditFilterActivity
|
import com.keylesspalace.tusky.components.filters.EditFilterActivity
|
||||||
import com.keylesspalace.tusky.components.filters.FiltersActivity
|
import com.keylesspalace.tusky.components.filters.FiltersActivity
|
||||||
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
|
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
|
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
|
@ -113,7 +113,7 @@ abstract class ActivitiesModule {
|
||||||
abstract fun contributesReportActivity(): ReportActivity
|
abstract fun contributesReportActivity(): ReportActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
abstract fun contributesInstanceListActivity(): DomainBlocksActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesScheduledStatusActivity(): ScheduledStatusActivity
|
abstract fun contributesScheduledStatusActivity(): ScheduledStatusActivity
|
||||||
|
|
|
@ -20,7 +20,7 @@ import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment
|
||||||
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
|
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
|
||||||
import com.keylesspalace.tusky.components.accountlist.AccountListFragment
|
import com.keylesspalace.tusky.components.accountlist.AccountListFragment
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||||
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
import com.keylesspalace.tusky.components.domainblocks.DomainBlocksFragment
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationsFragment
|
import com.keylesspalace.tusky.components.notifications.NotificationsFragment
|
||||||
import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment
|
import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment
|
||||||
import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment
|
import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment
|
||||||
|
@ -81,7 +81,7 @@ abstract class FragmentBuildersModule {
|
||||||
abstract fun reportDoneFragment(): ReportDoneFragment
|
abstract fun reportDoneFragment(): ReportDoneFragment
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun instanceListFragment(): InstanceListFragment
|
abstract fun instanceListFragment(): DomainBlocksFragment
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun searchStatusesFragment(): SearchStatusesFragment
|
abstract fun searchStatusesFragment(): SearchStatusesFragment
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.components.account.media.AccountMediaViewModel
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
|
import com.keylesspalace.tusky.components.domainblocks.DomainBlocksViewModel
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
||||||
import com.keylesspalace.tusky.components.filters.EditFilterViewModel
|
import com.keylesspalace.tusky.components.filters.EditFilterViewModel
|
||||||
import com.keylesspalace.tusky.components.filters.FiltersViewModel
|
import com.keylesspalace.tusky.components.filters.FiltersViewModel
|
||||||
|
@ -185,5 +186,10 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(EditFilterViewModel::class)
|
@ViewModelKey(EditFilterViewModel::class)
|
||||||
internal abstract fun editFilterViewModel(viewModel: EditFilterViewModel): ViewModel
|
internal abstract fun editFilterViewModel(viewModel: EditFilterViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(DomainBlocksViewModel::class)
|
||||||
|
internal abstract fun instanceMuteViewModel(viewModel: DomainBlocksViewModel): ViewModel
|
||||||
|
|
||||||
// Add more ViewModels here
|
// Add more ViewModels here
|
||||||
}
|
}
|
||||||
|
|
|
@ -457,11 +457,11 @@ interface MastodonApi {
|
||||||
): Response<List<TimelineAccount>>
|
): Response<List<TimelineAccount>>
|
||||||
|
|
||||||
@GET("api/v1/domain_blocks")
|
@GET("api/v1/domain_blocks")
|
||||||
fun domainBlocks(
|
suspend fun domainBlocks(
|
||||||
@Query("max_id") maxId: String? = null,
|
@Query("max_id") maxId: String? = null,
|
||||||
@Query("since_id") sinceId: String? = null,
|
@Query("since_id") sinceId: String? = null,
|
||||||
@Query("limit") limit: Int? = null
|
@Query("limit") limit: Int? = null
|
||||||
): Single<Response<List<String>>>
|
): Response<List<String>>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/domain_blocks")
|
@POST("api/v1/domain_blocks")
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/instanceProgressBar"
|
android:id="@+id/progressBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.BackgroundMessageView
|
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||||
android:id="@+id/messageView"
|
android:id="@+id/messageView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -11,7 +11,7 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/muted_domain_unmute"
|
android:id="@+id/blocked_domain_unblock"
|
||||||
style="@style/TuskyImageButton"
|
style="@style/TuskyImageButton"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/muted_domain"
|
android:id="@+id/blocked_domain"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
@ -38,4 +38,4 @@
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
tools:text="instance.domain.tld" />
|
tools:text="instance.domain.tld" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -45,6 +45,8 @@
|
||||||
<string name="error_following_hashtags_unsupported">This instance does not support following hashtags.</string>
|
<string name="error_following_hashtags_unsupported">This instance does not support following hashtags.</string>
|
||||||
<string name="error_muting_hashtag_format">Error muting #%s</string>
|
<string name="error_muting_hashtag_format">Error muting #%s</string>
|
||||||
<string name="error_unmuting_hashtag_format">Error unmuting #%s</string>
|
<string name="error_unmuting_hashtag_format">Error unmuting #%s</string>
|
||||||
|
<string name="error_blocking_domain">Failed to mute %1$s: %2$s</string>
|
||||||
|
<string name="error_unblocking_domain">Failed to unmute %1$s: %2$s</string>
|
||||||
<string name="error_status_source_load">Failed to load the status source from the server.</string>
|
<string name="error_status_source_load">Failed to load the status source from the server.</string>
|
||||||
|
|
||||||
<string name="title_login">Login</string>
|
<string name="title_login">Login</string>
|
||||||
|
|
Loading…
Reference in New Issue