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=".FiltersActivity" />
|
||||||
<activity android:name=".components.report.ReportActivity"
|
<activity android:name=".components.report.ReportActivity"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
|
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
private var avatarSize: Float = 0f
|
private var avatarSize: Float = 0f
|
||||||
@Px
|
@Px
|
||||||
private var titleVisibleHeight: Int = 0
|
private var titleVisibleHeight: Int = 0
|
||||||
|
private lateinit var domain: String
|
||||||
|
|
||||||
private enum class FollowState {
|
private enum class FollowState {
|
||||||
NOT_FOLLOWING,
|
NOT_FOLLOWING,
|
||||||
|
@ -601,6 +602,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
getString(R.string.action_mute)
|
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) {
|
if (followState == FollowState.FOLLOWING) {
|
||||||
val showReblogs = menu.findItem(R.id.action_show_reblogs)
|
val showReblogs = menu.findItem(R.id.action_show_reblogs)
|
||||||
showReblogs.title = if (showingReblogs) {
|
showReblogs.title = if (showingReblogs) {
|
||||||
|
@ -618,6 +630,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
menu.removeItem(R.id.action_follow)
|
menu.removeItem(R.id.action_follow)
|
||||||
menu.removeItem(R.id.action_block)
|
menu.removeItem(R.id.action_block)
|
||||||
menu.removeItem(R.id.action_mute)
|
menu.removeItem(R.id.action_mute)
|
||||||
|
menu.removeItem(R.id.action_mute_domain)
|
||||||
menu.removeItem(R.id.action_show_reblogs)
|
menu.removeItem(R.id.action_show_reblogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,6 +653,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
.show()
|
.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() {
|
private fun mention() {
|
||||||
loadedAccount?.let {
|
loadedAccount?.let {
|
||||||
val intent = ComposeActivity.IntentBuilder()
|
val intent = ComposeActivity.IntentBuilder()
|
||||||
|
@ -694,7 +715,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
viewModel.changeMuteState()
|
viewModel.changeMuteState()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.action_mute_domain -> {
|
||||||
|
showMuteDomainWarningDialog(domain)
|
||||||
|
return true
|
||||||
|
}
|
||||||
R.id.action_show_reblogs -> {
|
R.id.action_show_reblogs -> {
|
||||||
viewModel.changeShowReblogsState()
|
viewModel.changeShowReblogsState()
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -16,3 +16,4 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
||||||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : 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
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.*
|
||||||
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
@ -92,4 +93,7 @@ abstract class ActivitiesModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesReportActivity(): ReportActivity
|
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.AccountsInListFragment
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||||
|
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
||||||
import com.keylesspalace.tusky.fragment.*
|
import com.keylesspalace.tusky.fragment.*
|
||||||
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
||||||
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
|
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
|
||||||
|
@ -71,4 +72,7 @@ abstract class FragmentBuildersModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun reportDoneFragment(): ReportDoneFragment
|
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.StatusBaseViewHolder;
|
||||||
import com.keylesspalace.tusky.adapter.TimelineAdapter;
|
import com.keylesspalace.tusky.adapter.TimelineAdapter;
|
||||||
import com.keylesspalace.tusky.appstore.BlockEvent;
|
import com.keylesspalace.tusky.appstore.BlockEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.DomainMuteEvent;
|
||||||
import com.keylesspalace.tusky.appstore.EventHub;
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
import com.keylesspalace.tusky.appstore.FavoriteEvent;
|
import com.keylesspalace.tusky.appstore.FavoriteEvent;
|
||||||
import com.keylesspalace.tusky.appstore.MuteEvent;
|
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.TimelineRepository;
|
||||||
import com.keylesspalace.tusky.repository.TimelineRequestMode;
|
import com.keylesspalace.tusky.repository.TimelineRequestMode;
|
||||||
import com.keylesspalace.tusky.util.Either;
|
import com.keylesspalace.tusky.util.Either;
|
||||||
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
||||||
import com.keylesspalace.tusky.util.ListUtils;
|
import com.keylesspalace.tusky.util.ListUtils;
|
||||||
import com.keylesspalace.tusky.util.PairedList;
|
import com.keylesspalace.tusky.util.PairedList;
|
||||||
|
@ -526,6 +528,11 @@ public class TimelineFragment extends SFragment implements
|
||||||
String id = ((MuteEvent) event).getAccountId();
|
String id = ((MuteEvent) event).getAccountId();
|
||||||
removeAllByAccountId(id);
|
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) {
|
} else if (event instanceof StatusDeletedEvent) {
|
||||||
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
|
||||||
String id = ((StatusDeletedEvent) event).getStatusId();
|
String id = ((StatusDeletedEvent) event).getStatusId();
|
||||||
|
@ -870,6 +877,18 @@ public class TimelineFragment extends SFragment implements
|
||||||
updateAdapter();
|
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() {
|
private void onLoadMore() {
|
||||||
if (didLoadEverythingBottom || bottomLoading) {
|
if (didLoadEverythingBottom || bottomLoading) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.*
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
@ -59,6 +60,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
private lateinit var tabPreference: Preference
|
private lateinit var tabPreference: Preference
|
||||||
private lateinit var mutedUsersPreference: Preference
|
private lateinit var mutedUsersPreference: Preference
|
||||||
private lateinit var blockedUsersPreference: Preference
|
private lateinit var blockedUsersPreference: Preference
|
||||||
|
private lateinit var mutedDomainsPreference: Preference
|
||||||
|
|
||||||
private lateinit var defaultPostPrivacyPreference: ListPreference
|
private lateinit var defaultPostPrivacyPreference: ListPreference
|
||||||
private lateinit var defaultMediaSensitivityPreference: SwitchPreferenceCompat
|
private lateinit var defaultMediaSensitivityPreference: SwitchPreferenceCompat
|
||||||
|
@ -78,6 +80,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
tabPreference = requirePreference("tabPreference")
|
tabPreference = requirePreference("tabPreference")
|
||||||
mutedUsersPreference = requirePreference("mutedUsersPreference")
|
mutedUsersPreference = requirePreference("mutedUsersPreference")
|
||||||
blockedUsersPreference = requirePreference("blockedUsersPreference")
|
blockedUsersPreference = requirePreference("blockedUsersPreference")
|
||||||
|
mutedDomainsPreference = requirePreference("mutedDomainsPreference")
|
||||||
defaultPostPrivacyPreference = requirePreference("defaultPostPrivacy") as ListPreference
|
defaultPostPrivacyPreference = requirePreference("defaultPostPrivacy") as ListPreference
|
||||||
defaultMediaSensitivityPreference = requirePreference("defaultMediaSensitivity") as SwitchPreferenceCompat
|
defaultMediaSensitivityPreference = requirePreference("defaultMediaSensitivity") as SwitchPreferenceCompat
|
||||||
mediaPreviewEnabledPreference = requirePreference("mediaPreviewEnabled") 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))
|
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)
|
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))
|
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
|
notificationPreference.onPreferenceClickListener = this
|
||||||
tabPreference.onPreferenceClickListener = this
|
tabPreference.onPreferenceClickListener = this
|
||||||
mutedUsersPreference.onPreferenceClickListener = this
|
mutedUsersPreference.onPreferenceClickListener = this
|
||||||
blockedUsersPreference.onPreferenceClickListener = this
|
blockedUsersPreference.onPreferenceClickListener = this
|
||||||
|
mutedDomainsPreference.onPreferenceClickListener = this
|
||||||
homeFiltersPreference.onPreferenceClickListener = this
|
homeFiltersPreference.onPreferenceClickListener = this
|
||||||
notificationFiltersPreference.onPreferenceClickListener = this
|
notificationFiltersPreference.onPreferenceClickListener = this
|
||||||
publicFiltersPreference.onPreferenceClickListener = this
|
publicFiltersPreference.onPreferenceClickListener = this
|
||||||
|
@ -191,6 +196,12 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
||||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||||
true
|
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 -> {
|
homeFiltersPreference -> {
|
||||||
launchFilterActivity(Filter.HOME, R.string.title_home)
|
launchFilterActivity(Filter.HOME, R.string.title_home)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ import retrofit2.http.DELETE;
|
||||||
import retrofit2.http.Field;
|
import retrofit2.http.Field;
|
||||||
import retrofit2.http.FormUrlEncoded;
|
import retrofit2.http.FormUrlEncoded;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.HTTP;
|
||||||
import retrofit2.http.Header;
|
import retrofit2.http.Header;
|
||||||
import retrofit2.http.Multipart;
|
import retrofit2.http.Multipart;
|
||||||
import retrofit2.http.PATCH;
|
import retrofit2.http.PATCH;
|
||||||
|
@ -267,6 +268,21 @@ public interface MastodonApi {
|
||||||
@GET("api/v1/mutes")
|
@GET("api/v1/mutes")
|
||||||
Single<Response<List<Account>>> mutes(@Query("max_id") String maxId);
|
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")
|
@GET("api/v1/favourites")
|
||||||
Call<List<Status>> favourites(
|
Call<List<Status>> favourites(
|
||||||
@Query("max_id") String maxId,
|
@Query("max_id") String maxId,
|
||||||
|
|
|
@ -40,7 +40,7 @@ import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
public class LinkHelper {
|
public class LinkHelper {
|
||||||
private static String getDomain(String urlString) {
|
public static String getDomain(String urlString) {
|
||||||
URI uri;
|
URI uri;
|
||||||
try {
|
try {
|
||||||
uri = new URI(urlString);
|
uri = new URI(urlString);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.keylesspalace.tusky.viewmodel
|
package com.keylesspalace.tusky.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.keylesspalace.tusky.appstore.*
|
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() {
|
fun changeShowReblogsState() {
|
||||||
if (relationshipData.value?.data?.showingReblogs == true) {
|
if (relationshipData.value?.data?.showingReblogs == true) {
|
||||||
changeRelationship(RelationShipAction.FOLLOW, false)
|
changeRelationship(RelationShipAction.FOLLOW, false)
|
||||||
|
@ -226,4 +243,7 @@ class AccountViewModel @Inject constructor(
|
||||||
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE
|
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"
|
android:title="@string/action_block"
|
||||||
app:showAsAction="never" />
|
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"
|
<item android:id="@+id/action_show_reblogs"
|
||||||
android:title="@string/action_hide_reblogs"
|
android:title="@string/action_hide_reblogs"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
<string name="title_favourites">Favourites</string>
|
<string name="title_favourites">Favourites</string>
|
||||||
<string name="title_mutes">Muted users</string>
|
<string name="title_mutes">Muted users</string>
|
||||||
<string name="title_blocks">Blocked 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_follow_requests">Follow Requests</string>
|
||||||
<string name="title_edit_profile">Edit your profile</string>
|
<string name="title_edit_profile">Edit your profile</string>
|
||||||
<string name="title_saved_toot">Drafts</string>
|
<string name="title_saved_toot">Drafts</string>
|
||||||
|
@ -92,6 +93,7 @@
|
||||||
<string name="action_view_favourites">Favourites</string>
|
<string name="action_view_favourites">Favourites</string>
|
||||||
<string name="action_view_mutes">Muted users</string>
|
<string name="action_view_mutes">Muted users</string>
|
||||||
<string name="action_view_blocks">Blocked 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_follow_requests">Follow Requests</string>
|
||||||
<string name="action_view_media">Media</string>
|
<string name="action_view_media">Media</string>
|
||||||
<string name="action_open_in_web">Open in browser</string>
|
<string name="action_open_in_web">Open in browser</string>
|
||||||
|
@ -100,6 +102,7 @@
|
||||||
<string name="action_share">Share</string>
|
<string name="action_share">Share</string>
|
||||||
<string name="action_mute">Mute</string>
|
<string name="action_mute">Mute</string>
|
||||||
<string name="action_unmute">Unmute</string>
|
<string name="action_unmute">Unmute</string>
|
||||||
|
<string name="action_mute_domain">Mute %s</string>
|
||||||
<string name="action_mention">Mention</string>
|
<string name="action_mention">Mention</string>
|
||||||
<string name="action_hide_media">Hide media</string>
|
<string name="action_hide_media">Hide media</string>
|
||||||
<string name="action_open_drawer">Open drawer</string>
|
<string name="action_open_drawer">Open drawer</string>
|
||||||
|
@ -142,6 +145,7 @@
|
||||||
<string name="confirmation_reported">Sent!</string>
|
<string name="confirmation_reported">Sent!</string>
|
||||||
<string name="confirmation_unblocked">User unblocked</string>
|
<string name="confirmation_unblocked">User unblocked</string>
|
||||||
<string name="confirmation_unmuted">User unmuted</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">Sent!</string>
|
||||||
<string name="status_sent_long">Reply sent successfully.</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_unfollow_warning">Unfollow this account?</string>
|
||||||
<string name="dialog_delete_toot_warning">Delete this toot?</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="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_public">Public: Post to public timelines</string>
|
||||||
<string name="visibility_unlisted">Unlisted: Do not show in public timelines</string>
|
<string name="visibility_unlisted">Unlisted: Do not show in public timelines</string>
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
android:key="blockedUsersPreference"
|
android:key="blockedUsersPreference"
|
||||||
android:title="@string/action_view_blocks" />
|
android:title="@string/action_view_blocks" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="mutedDomainsPreference"
|
||||||
|
android:title="@string/action_view_domain_mutes" />
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/pref_publishing">
|
<PreferenceCategory android:title="@string/pref_publishing">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
android:defaultValue="public"
|
android:defaultValue="public"
|
||||||
|
|
Loading…
Reference in a new issue