Implement instance mutes (#1311)
* Implement instance mutes. #1143 * Move new classes to instancemute component * Add progress bar while instance list loads * Add undo snackbar for instance unmuting * Update display text for instance mutes
This commit is contained in:
parent
c10f3bce24
commit
a6819ce28e
20 changed files with 494 additions and 5 deletions
|
@ -119,6 +119,7 @@
|
|||
<activity android:name=".FiltersActivity" />
|
||||
<activity android:name=".components.report.ReportActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
|
||||
|
@ -160,4 +161,4 @@
|
|||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
@ -93,6 +93,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
private var avatarSize: Float = 0f
|
||||
@Px
|
||||
private var titleVisibleHeight: Int = 0
|
||||
private lateinit var domain: String
|
||||
|
||||
private enum class FollowState {
|
||||
NOT_FOLLOWING,
|
||||
|
@ -601,6 +602,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
getString(R.string.action_mute)
|
||||
}
|
||||
|
||||
if (loadedAccount != null) {
|
||||
val muteDomain = menu.findItem(R.id.action_mute_domain)
|
||||
domain = LinkHelper.getDomain(loadedAccount?.url)
|
||||
if (domain.isEmpty()) {
|
||||
// If we can't get the domain, there's no way we can mute it anyway...
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
} else {
|
||||
muteDomain.title = getString(R.string.action_mute_domain, domain)
|
||||
}
|
||||
}
|
||||
|
||||
if (followState == FollowState.FOLLOWING) {
|
||||
val showReblogs = menu.findItem(R.id.action_show_reblogs)
|
||||
showReblogs.title = if (showingReblogs) {
|
||||
|
@ -618,6 +630,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
menu.removeItem(R.id.action_follow)
|
||||
menu.removeItem(R.id.action_block)
|
||||
menu.removeItem(R.id.action_mute)
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
menu.removeItem(R.id.action_show_reblogs)
|
||||
}
|
||||
|
||||
|
@ -640,6 +653,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun showMuteDomainWarningDialog(instance: String) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.mute_domain_warning, instance))
|
||||
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.muteDomain(instance) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun mention() {
|
||||
loadedAccount?.let {
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
|
@ -694,7 +715,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
viewModel.changeMuteState()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_mute_domain -> {
|
||||
showMuteDomainWarningDialog(domain)
|
||||
return true
|
||||
}
|
||||
R.id.action_show_reblogs -> {
|
||||
viewModel.changeShowReblogsState()
|
||||
return true
|
||||
|
|
|
@ -15,4 +15,5 @@ data class StatusComposedEvent(val status: Status) : Dispatchable
|
|||
data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
||||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
|
@ -0,0 +1,50 @@
|
|||
package com.keylesspalace.tusky.components.instancemute
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.support.HasSupportFragmentInjector
|
||||
import javax.inject.Inject
|
||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||
|
||||
class InstanceListActivity: BaseActivity(), HasSupportFragmentInjector {
|
||||
@Inject
|
||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
|
||||
|
||||
override fun supportFragmentInjector(): AndroidInjector<Fragment>? {
|
||||
return dispatchingAndroidInjector
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_account_list)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setTitle(R.string.title_domain_mutes)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, InstanceListFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package com.keylesspalace.tusky.components.instancemute.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
|
||||
import kotlinx.android.synthetic.main.item_muted_domain.view.*
|
||||
|
||||
class DomainMutesAdapter(private val actionListener: InstanceActionListener): RecyclerView.Adapter<DomainMutesAdapter.ViewHolder>() {
|
||||
var instances: MutableList<String> = mutableListOf()
|
||||
var bottomLoading: Boolean = false
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_muted_domain, parent, false), actionListener)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.setupWithInstance(instances[position])
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ViewHolder(rootView: View, private val actionListener: InstanceActionListener): RecyclerView.ViewHolder(rootView) {
|
||||
fun setupWithInstance(instance: String) {
|
||||
itemView.muted_domain.text = instance
|
||||
itemView.muted_domain_unmute.setOnClickListener {
|
||||
actionListener.mute(false, instance, adapterPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
package com.keylesspalace.tusky.components.instancemute.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.di.Injectable
|
||||
import com.keylesspalace.tusky.fragment.BaseFragment
|
||||
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.view.EndlessOnScrollListener
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_instance_list.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener {
|
||||
@Inject
|
||||
lateinit var api: MastodonApi
|
||||
|
||||
private var fetching = false
|
||||
private var bottomId: String? = null
|
||||
private var adapter = DomainMutesAdapter(this)
|
||||
private lateinit var scrollListener: EndlessOnScrollListener
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_instance_list, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(view.context)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
|
||||
scrollListener = object : EndlessOnScrollListener(layoutManager) {
|
||||
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
|
||||
if (bottomId != null) {
|
||||
fetchInstances(bottomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener)
|
||||
fetchInstances()
|
||||
}
|
||||
|
||||
override fun mute(mute: Boolean, instance: String, position: Int) {
|
||||
if (mute) {
|
||||
api.blockDomain(instance).enqueue(object: Callback<Any> {
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error muting domain $instance")
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
if (response.isSuccessful) {
|
||||
adapter.addItem(instance)
|
||||
} else {
|
||||
Log.e(TAG, "Error muting domain $instance")
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
api.unblockDomain(instance).enqueue(object: Callback<Any> {
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error unmuting domain $instance")
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
if (response.isSuccessful) {
|
||||
adapter.removeItem(position)
|
||||
Snackbar.make(recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_undo) {
|
||||
mute(true, instance, position)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
Log.e(TAG, "Error unmuting domain $instance")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchInstances(id: String? = null) {
|
||||
if (fetching) {
|
||||
return
|
||||
}
|
||||
fetching = true
|
||||
instanceProgressBar.show()
|
||||
|
||||
if (id != null) {
|
||||
recyclerView.post { adapter.bottomLoading = true }
|
||||
}
|
||||
|
||||
api.domainBlocks(id, bottomId, null)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ response ->
|
||||
val instances = response.body()
|
||||
|
||||
if (response.isSuccessful && instances != null) {
|
||||
onFetchInstancesSuccess(instances, response.headers().get("Link"))
|
||||
} else {
|
||||
onFetchInstancesFailure(Exception(response.message()))
|
||||
}
|
||||
}, {throwable ->
|
||||
onFetchInstancesFailure(throwable)
|
||||
})
|
||||
}
|
||||
|
||||
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
|
||||
adapter.bottomLoading = false
|
||||
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) {
|
||||
messageView.show()
|
||||
messageView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.message_empty,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
messageView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFetchInstancesFailure(throwable: Throwable) {
|
||||
fetching = false
|
||||
instanceProgressBar.hide()
|
||||
Log.e(TAG, "Fetch failure", throwable)
|
||||
|
||||
if (adapter.itemCount == 0) {
|
||||
messageView.show()
|
||||
if (throwable is IOException) {
|
||||
messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||
messageView.hide()
|
||||
this.fetchInstances(null)
|
||||
}
|
||||
} else {
|
||||
messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
messageView.hide()
|
||||
this.fetchInstances(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "InstanceList" // logging tag
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.keylesspalace.tusky.components.instancemute.interfaces
|
||||
|
||||
interface InstanceActionListener {
|
||||
fun mute(mute: Boolean, instance: String, position: Int)
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.*
|
||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
@ -92,4 +93,7 @@ abstract class ActivitiesModule {
|
|||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesReportActivity(): ReportActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di
|
|||
|
||||
import com.keylesspalace.tusky.AccountsInListFragment
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
||||
import com.keylesspalace.tusky.fragment.*
|
||||
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
||||
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
|
||||
|
@ -71,4 +72,7 @@ abstract class FragmentBuildersModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun reportDoneFragment(): ReportDoneFragment
|
||||
}
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun instanceListFragment(): InstanceListFragment
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.keylesspalace.tusky.R;
|
|||
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
|
||||
import com.keylesspalace.tusky.adapter.TimelineAdapter;
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent;
|
||||
import com.keylesspalace.tusky.appstore.DomainMuteEvent;
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.FavoriteEvent;
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent;
|
||||
|
@ -56,6 +57,7 @@ import com.keylesspalace.tusky.repository.Placeholder;
|
|||
import com.keylesspalace.tusky.repository.TimelineRepository;
|
||||
import com.keylesspalace.tusky.repository.TimelineRequestMode;
|
||||
import com.keylesspalace.tusky.util.Either;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
||||
import com.keylesspalace.tusky.util.ListUtils;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
|
@ -526,6 +528,11 @@ public class TimelineFragment extends SFragment implements
|
|||
String id = ((MuteEvent) event).getAccountId();
|
||||
removeAllByAccountId(id);
|
||||
}
|
||||
} else if (event instanceof DomainMuteEvent) {
|
||||
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
||||
String instance = ((DomainMuteEvent) event).getInstance();
|
||||
removeAllByInstance(instance);
|
||||
}
|
||||
} else if (event instanceof StatusDeletedEvent) {
|
||||
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
||||
String id = ((StatusDeletedEvent) event).getStatusId();
|
||||
|
@ -870,6 +877,18 @@ public class TimelineFragment extends SFragment implements
|
|||
updateAdapter();
|
||||
}
|
||||
|
||||
private void removeAllByInstance(String instance) {
|
||||
// using iterator to safely remove items while iterating
|
||||
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Status status = iterator.next().asRightOrNull();
|
||||
if (status != null && LinkHelper.getDomain(status.getAccount().getUrl()).equals(instance)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
updateAdapter();
|
||||
}
|
||||
|
||||
private void onLoadMore() {
|
||||
if (didLoadEverythingBottom || bottomLoading) {
|
||||
return;
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.*
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
|
@ -59,6 +60,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
private lateinit var tabPreference: Preference
|
||||
private lateinit var mutedUsersPreference: Preference
|
||||
private lateinit var blockedUsersPreference: Preference
|
||||
private lateinit var mutedDomainsPreference: Preference
|
||||
|
||||
private lateinit var defaultPostPrivacyPreference: ListPreference
|
||||
private lateinit var defaultMediaSensitivityPreference: SwitchPreferenceCompat
|
||||
|
@ -78,6 +80,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
tabPreference = requirePreference("tabPreference")
|
||||
mutedUsersPreference = requirePreference("mutedUsersPreference")
|
||||
blockedUsersPreference = requirePreference("blockedUsersPreference")
|
||||
mutedDomainsPreference = requirePreference("mutedDomainsPreference")
|
||||
defaultPostPrivacyPreference = requirePreference("defaultPostPrivacy") as ListPreference
|
||||
defaultMediaSensitivityPreference = requirePreference("defaultMediaSensitivity") as SwitchPreferenceCompat
|
||||
mediaPreviewEnabledPreference = requirePreference("mediaPreviewEnabled") as SwitchPreferenceCompat
|
||||
|
@ -90,11 +93,13 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
notificationPreference.icon = IconicsDrawable(notificationPreference.context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(notificationPreference.context, R.attr.toolbar_icon_tint))
|
||||
mutedUsersPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
|
||||
blockedUsersPreference.icon = IconicsDrawable(blockedUsersPreference.context, GoogleMaterial.Icon.gmd_block).sizePx(iconSize).color(ThemeUtils.getColor(blockedUsersPreference.context, R.attr.toolbar_icon_tint))
|
||||
mutedDomainsPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
|
||||
|
||||
notificationPreference.onPreferenceClickListener = this
|
||||
tabPreference.onPreferenceClickListener = this
|
||||
mutedUsersPreference.onPreferenceClickListener = this
|
||||
blockedUsersPreference.onPreferenceClickListener = this
|
||||
mutedDomainsPreference.onPreferenceClickListener = this
|
||||
homeFiltersPreference.onPreferenceClickListener = this
|
||||
notificationFiltersPreference.onPreferenceClickListener = this
|
||||
publicFiltersPreference.onPreferenceClickListener = this
|
||||
|
@ -191,6 +196,12 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
true
|
||||
}
|
||||
mutedDomainsPreference -> {
|
||||
val intent = Intent(context, InstanceListActivity::class.java)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
true
|
||||
}
|
||||
homeFiltersPreference -> {
|
||||
launchFilterActivity(Filter.HOME, R.string.title_home)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import retrofit2.http.DELETE;
|
|||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.PATCH;
|
||||
|
@ -267,6 +268,21 @@ public interface MastodonApi {
|
|||
@GET("api/v1/mutes")
|
||||
Single<Response<List<Account>>> mutes(@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/domain_blocks")
|
||||
Single<Response<List<String>>> domainBlocks(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/domain_blocks")
|
||||
Call<Object> blockDomain(@Field("domain") String domain);
|
||||
|
||||
@FormUrlEncoded
|
||||
// Normal @DELETE doesn't support fields?
|
||||
@HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true)
|
||||
Call<Object> unblockDomain(@Field("domain") String domain);
|
||||
|
||||
@GET("api/v1/favourites")
|
||||
Call<List<Status>> favourites(
|
||||
@Query("max_id") String maxId,
|
||||
|
|
|
@ -40,7 +40,7 @@ import java.net.URI;
|
|||
import java.net.URISyntaxException;
|
||||
|
||||
public class LinkHelper {
|
||||
private static String getDomain(String urlString) {
|
||||
public static String getDomain(String urlString) {
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(urlString);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.keylesspalace.tusky.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
|
@ -122,6 +123,22 @@ class AccountViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun muteDomain(instance: String) {
|
||||
mastodonApi.blockDomain(instance).enqueue(object: Callback<Any> {
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
if (response.isSuccessful) {
|
||||
eventHub.dispatch(DomainMuteEvent(instance))
|
||||
} else {
|
||||
Log.e(TAG, String.format("Error muting %s", instance))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, String.format("Error muting %s", instance), t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun changeShowReblogsState() {
|
||||
if (relationshipData.value?.data?.showingReblogs == true) {
|
||||
changeRelationship(RelationShipAction.FOLLOW, false)
|
||||
|
@ -226,4 +243,7 @@ class AccountViewModel @Inject constructor(
|
|||
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "AccountViewModel"
|
||||
}
|
||||
}
|
18
app/src/main/res/layout/activity_instance_list.xml
Normal file
18
app/src/main/res/layout/activity_instance_list.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/activity_instance_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.keylesspalace.tusky.InstanceListActivity">
|
||||
|
||||
<include layout="@layout/toolbar_basic" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
25
app/src/main/res/layout/fragment_instance_list.xml
Normal file
25
app/src/main/res/layout/fragment_instance_list.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/instanceProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||
android:id="@+id/messageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
41
app/src/main/res/layout/item_muted_domain.xml
Normal file
41
app/src/main/res/layout/item_muted_domain.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/muted_domain_unmute"
|
||||
style="?attr/image_button_style"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_unmute"
|
||||
android:padding="4dp"
|
||||
app:srcCompat="@drawable/ic_unmute_24dp"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/muted_domain"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:gravity="center_vertical"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
tools:text="instance.domain.tld" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -22,6 +22,10 @@
|
|||
android:title="@string/action_block"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/action_mute_domain"
|
||||
android:title="@string/action_mute_domain"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/action_show_reblogs"
|
||||
android:title="@string/action_hide_reblogs"
|
||||
app:showAsAction="never" />
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
<string name="title_favourites">Favourites</string>
|
||||
<string name="title_mutes">Muted users</string>
|
||||
<string name="title_blocks">Blocked users</string>
|
||||
<string name="title_domain_mutes">Hidden domains</string>
|
||||
<string name="title_follow_requests">Follow Requests</string>
|
||||
<string name="title_edit_profile">Edit your profile</string>
|
||||
<string name="title_saved_toot">Drafts</string>
|
||||
|
@ -92,6 +93,7 @@
|
|||
<string name="action_view_favourites">Favourites</string>
|
||||
<string name="action_view_mutes">Muted users</string>
|
||||
<string name="action_view_blocks">Blocked users</string>
|
||||
<string name="action_view_domain_mutes">Hidden domains</string>
|
||||
<string name="action_view_follow_requests">Follow Requests</string>
|
||||
<string name="action_view_media">Media</string>
|
||||
<string name="action_open_in_web">Open in browser</string>
|
||||
|
@ -100,6 +102,7 @@
|
|||
<string name="action_share">Share</string>
|
||||
<string name="action_mute">Mute</string>
|
||||
<string name="action_unmute">Unmute</string>
|
||||
<string name="action_mute_domain">Mute %s</string>
|
||||
<string name="action_mention">Mention</string>
|
||||
<string name="action_hide_media">Hide media</string>
|
||||
<string name="action_open_drawer">Open drawer</string>
|
||||
|
@ -142,6 +145,7 @@
|
|||
<string name="confirmation_reported">Sent!</string>
|
||||
<string name="confirmation_unblocked">User unblocked</string>
|
||||
<string name="confirmation_unmuted">User unmuted</string>
|
||||
<string name="confirmation_domain_unmuted">%s unhidden</string>
|
||||
|
||||
<string name="status_sent">Sent!</string>
|
||||
<string name="status_sent_long">Reply sent successfully.</string>
|
||||
|
@ -179,6 +183,8 @@
|
|||
<string name="dialog_unfollow_warning">Unfollow this account?</string>
|
||||
<string name="dialog_delete_toot_warning">Delete this toot?</string>
|
||||
<string name="dialog_redraft_toot_warning">Delete and re-draft this toot?</string>
|
||||
<string name="mute_domain_warning">Are you sure you want to block all of %s? You will not see content from that domain in any public timelines or in your notifications. Your followers from that domain will be removed.</string>
|
||||
<string name="mute_domain_warning_dialog_ok">Hide entire domain</string>
|
||||
|
||||
<string name="visibility_public">Public: Post to public timelines</string>
|
||||
<string name="visibility_unlisted">Unlisted: Do not show in public timelines</string>
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
android:key="blockedUsersPreference"
|
||||
android:title="@string/action_view_blocks" />
|
||||
|
||||
<Preference
|
||||
android:key="mutedDomainsPreference"
|
||||
android:title="@string/action_view_domain_mutes" />
|
||||
|
||||
<PreferenceCategory android:title="@string/pref_publishing">
|
||||
<ListPreference
|
||||
android:defaultValue="public"
|
||||
|
|
Loading…
Reference in a new issue