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:
Levi Bard 2023-09-06 10:11:17 +02:00 committed by GitHub
commit 84969586c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 384 additions and 278 deletions

View File

@ -872,7 +872,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="777"
line="780"
column="5"/>
</issue>
@ -1928,7 +1928,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="450"
line="453"
column="13"/>
</issue>
@ -2071,7 +2071,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="615"
line="618"
column="13"/>
</issue>
@ -2082,7 +2082,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="617"
line="620"
column="13"/>
</issue>
@ -2093,7 +2093,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="618"
line="621"
column="13"/>
</issue>
@ -2104,7 +2104,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="621"
line="624"
column="13"/>
</issue>
@ -2115,7 +2115,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="666"
line="669"
column="13"/>
</issue>
@ -2126,7 +2126,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="715"
line="718"
column="13"/>
</issue>
@ -2137,7 +2137,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="721"
line="724"
column="13"/>
</issue>
@ -2148,7 +2148,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="762"
line="765"
column="13"/>
</issue>
@ -2159,7 +2159,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="798"
line="801"
column="13"/>
</issue>
@ -2734,17 +2734,7 @@
line="202"
column="21"/>
</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"
column="21"/>
</issue>
<issue
id="SyntheticAccessor"
@ -4242,13 +4232,6 @@
column="54"/>
</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
id="SelectableText"
message="Consider making the text value selectable by specifying `android:textIsSelectable=&quot;true&quot;`"
@ -4568,6 +4551,17 @@
column="2"/>
</issue>
<issue
id="SelectableText"
message="Consider making the text value selectable by specifying `android:textIsSelectable=&quot;true&quot;`"
errorLine1=" &lt;TextView"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/item_blocked_domain.xml"
line="27"
column="6"/>
</issue>
<issue
id="SelectableText"
message="Consider making the text value selectable by specifying `android:textIsSelectable=&quot;true&quot;`"
@ -4777,17 +4771,6 @@
column="6"/>
</issue>
<issue
id="SelectableText"
message="Consider making the text value selectable by specifying `android:textIsSelectable=&quot;true&quot;`"
errorLine1=" &lt;TextView"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/item_muted_domain.xml"
line="27"
column="6"/>
</issue>
<issue
id="SelectableText"
message="Consider making the text value selectable by specifying `android:textIsSelectable=&quot;true&quot;`"

View File

@ -148,7 +148,7 @@
<activity
android:name=".components.report.ReportActivity"
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.announcements.AnnouncementsActivity" />
<activity android:name=".components.drafts.DraftsActivity" />

View File

@ -61,7 +61,6 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import retrofit2.Response
import java.io.IOException
import javax.inject.Inject
class AccountListFragment :

View File

@ -1,15 +1,14 @@
package com.keylesspalace.tusky.components.instancemute
package com.keylesspalace.tusky.components.domainblocks
import android.os.Bundle
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class InstanceListActivity : BaseActivity(), HasAndroidInjector {
class DomainBlocksActivity : BaseActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -28,7 +27,7 @@ class InstanceListActivity : BaseActivity(), HasAndroidInjector {
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, InstanceListFragment())
.replace(R.id.fragment_container, DomainBlocksFragment())
.commit()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
package com.keylesspalace.tusky.components.instancemute.interfaces
interface InstanceActionListener {
fun mute(mute: Boolean, instance: String, position: Int)
}

View File

@ -30,9 +30,9 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.appstore.EventHub
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.followedtags.FollowedTagsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
import com.keylesspalace.tusky.db.AccountManager
@ -156,7 +156,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.title_domain_mutes)
setIcon(R.drawable.ic_mute_24dp)
setOnPreferenceClickListener {
val intent = Intent(context, InstanceListActivity::class.java)
val intent = Intent(context, DomainBlocksActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(
R.anim.slide_from_right,

View File

@ -29,11 +29,11 @@ import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
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.filters.EditFilterActivity
import com.keylesspalace.tusky.components.filters.FiltersActivity
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.LoginWebViewActivity
import com.keylesspalace.tusky.components.preference.PreferencesActivity
@ -113,7 +113,7 @@ abstract class ActivitiesModule {
abstract fun contributesReportActivity(): ReportActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesInstanceListActivity(): InstanceListActivity
abstract fun contributesInstanceListActivity(): DomainBlocksActivity
@ContributesAndroidInjector
abstract fun contributesScheduledStatusActivity(): ScheduledStatusActivity

View File

@ -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.accountlist.AccountListFragment
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.preference.AccountPreferencesFragment
import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment
@ -81,7 +81,7 @@ abstract class FragmentBuildersModule {
abstract fun reportDoneFragment(): ReportDoneFragment
@ContributesAndroidInjector
abstract fun instanceListFragment(): InstanceListFragment
abstract fun instanceListFragment(): DomainBlocksFragment
@ContributesAndroidInjector
abstract fun searchStatusesFragment(): SearchStatusesFragment

View File

@ -27,6 +27,7 @@ import com.keylesspalace.tusky.components.account.media.AccountMediaViewModel
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
import com.keylesspalace.tusky.components.compose.ComposeViewModel
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.filters.EditFilterViewModel
import com.keylesspalace.tusky.components.filters.FiltersViewModel
@ -185,5 +186,10 @@ abstract class ViewModelModule {
@ViewModelKey(EditFilterViewModel::class)
internal abstract fun editFilterViewModel(viewModel: EditFilterViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(DomainBlocksViewModel::class)
internal abstract fun instanceMuteViewModel(viewModel: DomainBlocksViewModel): ViewModel
// Add more ViewModels here
}

View File

@ -457,11 +457,11 @@ interface MastodonApi {
): Response<List<TimelineAccount>>
@GET("api/v1/domain_blocks")
fun domainBlocks(
suspend fun domainBlocks(
@Query("max_id") maxId: String? = null,
@Query("since_id") sinceId: String? = null,
@Query("limit") limit: Int? = null
): Single<Response<List<String>>>
): Response<List<String>>
@FormUrlEncoded
@POST("api/v1/domain_blocks")

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/instanceProgressBar"
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
@ -17,9 +17,9 @@
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/messageView"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</FrameLayout>

View File

@ -11,7 +11,7 @@
>
<ImageButton
android:id="@+id/muted_domain_unmute"
android:id="@+id/blocked_domain_unblock"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
@ -25,7 +25,7 @@
/>
<TextView
android:id="@+id/muted_domain"
android:id="@+id/blocked_domain"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
@ -38,4 +38,4 @@
android:textSize="?attr/status_text_medium"
tools:text="instance.domain.tld" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -45,6 +45,8 @@
<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_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="title_login">Login</string>