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