diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml
index 8dd9f6cf7..fb5904550 100644
--- a/app/lint-baseline.xml
+++ b/app/lint-baseline.xml
@@ -872,7 +872,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -1928,7 +1928,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2071,7 +2071,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
@@ -2082,7 +2082,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2093,7 +2093,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2104,7 +2104,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2115,7 +2115,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
@@ -2126,7 +2126,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2137,7 +2137,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2148,7 +2148,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2159,7 +2159,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -2734,17 +2734,7 @@
line="202"
column="21"/>
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt
index c3528efab..e08429aad 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt
@@ -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 :
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksActivity.kt
similarity index 78%
rename from app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksActivity.kt
index 667360c53..618174907 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksActivity.kt
@@ -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
@@ -28,7 +27,7 @@ class InstanceListActivity : BaseActivity(), HasAndroidInjector {
supportFragmentManager
.beginTransaction()
- .replace(R.id.fragment_container, InstanceListFragment())
+ .replace(R.id.fragment_container, DomainBlocksFragment())
.commit()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt
new file mode 100644
index 000000000..e37aa917c
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt
@@ -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_COMPARATOR) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
+ val binding = ItemBlockedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return BindingHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: BindingHolder, position: Int) {
+ getItem(position)?.let { instance ->
+ holder.binding.blockedDomain.text = instance
+ holder.binding.blockedDomainUnblock.setOnClickListener {
+ onUnmute(instance)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt
new file mode 100644
index 000000000..896e81ead
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt
@@ -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"
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksPagingSource.kt
new file mode 100644
index 000000000..0438a268f
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksPagingSource.kt
@@ -0,0 +1,19 @@
+package com.keylesspalace.tusky.components.domainblocks
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+
+class DomainBlocksPagingSource(
+ private val domains: List,
+ private val nextKey: String?
+) : PagingSource() {
+ override fun getRefreshKey(state: PagingState): String? = null
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ return if (params is LoadParams.Refresh) {
+ LoadResult.Page(domains, null, nextKey)
+ } else {
+ LoadResult.Page(emptyList(), null, null)
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRemoteMediator.kt
new file mode 100644
index 000000000..09f99044e
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRemoteMediator.kt
@@ -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() {
+
+ override suspend fun load(
+ loadType: LoadType,
+ state: PagingState
+ ): 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>? {
+ 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>): 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)
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRepository.kt
new file mode 100644
index 000000000..bdc9b9367
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRepository.kt
@@ -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 .
+ */
+
+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 = 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 {
+ return api.blockDomain(domain).onSuccess {
+ domains.add(domain)
+ factory.invalidate()
+ }
+ }
+
+ suspend fun unblock(domain: String): NetworkResult {
+ return api.unblockDomain(domain).onSuccess {
+ domains.remove(domain)
+ factory.invalidate()
+ }
+ }
+
+ companion object {
+ private const val PAGE_SIZE = 20
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt
new file mode 100644
index 000000000..6458977f0
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt
@@ -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()
+
+ 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
+)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt
deleted file mode 100644
index 13d8f2d83..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt
+++ /dev/null
@@ -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>() {
-
- var instances: MutableList = mutableListOf()
- var bottomLoading: Boolean = false
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
- val binding = ItemMutedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- return BindingHolder(binding)
- }
-
- override fun onBindViewHolder(holder: BindingHolder, 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) {
- 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)
- }
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt
deleted file mode 100644
index 1da0a2b7d..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt
+++ /dev/null
@@ -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, 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
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt
deleted file mode 100644
index 9b88ad966..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.keylesspalace.tusky.components.instancemute.interfaces
-
-interface InstanceActionListener {
- fun mute(mute: Boolean, instance: String, position: Int)
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
index b0fedb2b1..629c3709d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
@@ -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,
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
index 214ddcb48..f60c78f8a 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -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
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
index 7629cff9f..710ab75af 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
@@ -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
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
index ae69e1dd9..0ac5ae54e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
@@ -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
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
index 043b54d9a..3976f5395 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@@ -457,11 +457,11 @@ interface MastodonApi {
): Response>
@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>
@FormUrlEncoded
@POST("api/v1/domain_blocks")
diff --git a/app/src/main/res/layout/fragment_instance_list.xml b/app/src/main/res/layout/fragment_domain_blocks.xml
similarity index 88%
rename from app/src/main/res/layout/fragment_instance_list.xml
rename to app/src/main/res/layout/fragment_domain_blocks.xml
index 8270cee33..65fdf1d3e 100644
--- a/app/src/main/res/layout/fragment_instance_list.xml
+++ b/app/src/main/res/layout/fragment_domain_blocks.xml
@@ -5,7 +5,7 @@
android:layout_height="match_parent">
@@ -17,9 +17,9 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/item_muted_domain.xml b/app/src/main/res/layout/item_blocked_domain.xml
similarity index 91%
rename from app/src/main/res/layout/item_muted_domain.xml
rename to app/src/main/res/layout/item_blocked_domain.xml
index 3147fca56..e043e1a3b 100644
--- a/app/src/main/res/layout/item_muted_domain.xml
+++ b/app/src/main/res/layout/item_blocked_domain.xml
@@ -11,7 +11,7 @@
>
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0716cb6d8..b20e0b3d1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -45,6 +45,8 @@
This instance does not support following hashtags.
Error muting #%s
Error unmuting #%s
+ Failed to mute %1$s: %2$s
+ Failed to unmute %1$s: %2$s
Failed to load the status source from the server.
Login