diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 07acb061..30b1a796 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -119,6 +119,7 @@ + @@ -160,4 +161,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 36ff17d1..19114295 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index ecec9412..d7d92d49 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -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) : Dispatchable -data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable \ No newline at end of file +data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable +data class DomainMuteEvent(val instance: String): Dispatchable \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt new file mode 100644 index 00000000..7aa39bef --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt @@ -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 + + override fun supportFragmentInjector(): AndroidInjector? { + 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) + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..62ab7ef3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt @@ -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() { + var instances: MutableList = 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) { + 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) + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..575667b6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -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 { + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Error muting domain $instance") + } + + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + adapter.addItem(instance) + } else { + Log.e(TAG, "Error muting domain $instance") + } + } + }) + } else { + api.unblockDomain(instance).enqueue(object: Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Error unmuting domain $instance") + } + + override fun onResponse(call: Call, response: Response) { + 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, 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 + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..97d59cc9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt @@ -0,0 +1,5 @@ +package com.keylesspalace.tusky.components.instancemute.interfaces + +interface InstanceActionListener { + fun mute(mute: Boolean, instance: String, position: Int) +} \ No newline at end of file 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 f516ae16..4d6e1eaf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -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 } 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 05f618a9..21101fb1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -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 -} \ No newline at end of file + + @ContributesAndroidInjector + abstract fun instanceListFragment(): InstanceListFragment +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 2c09f1eb..ac151f1a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -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> 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; diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt index 2da07159..409f5947 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt @@ -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) } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index c429e272..4890ca89 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -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>> mutes(@Query("max_id") String maxId); + @GET("api/v1/domain_blocks") + Single>> domainBlocks( + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); + + @FormUrlEncoded + @POST("api/v1/domain_blocks") + Call blockDomain(@Field("domain") String domain); + + @FormUrlEncoded + // Normal @DELETE doesn't support fields? + @HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true) + Call unblockDomain(@Field("domain") String domain); + @GET("api/v1/favourites") Call> favourites( @Query("max_id") String maxId, diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index 324d17ff..929c0ac4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -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); diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index 815db8bf..adbcaa43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -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 { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + eventHub.dispatch(DomainMuteEvent(instance)) + } else { + Log.e(TAG, String.format("Error muting %s", instance)) + } + } + + override fun onFailure(call: Call, 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" + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_instance_list.xml b/app/src/main/res/layout/activity_instance_list.xml new file mode 100644 index 00000000..c8ca4a0a --- /dev/null +++ b/app/src/main/res/layout/activity_instance_list.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_instance_list.xml b/app/src/main/res/layout/fragment_instance_list.xml new file mode 100644 index 00000000..8270cee3 --- /dev/null +++ b/app/src/main/res/layout/fragment_instance_list.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ 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_muted_domain.xml new file mode 100644 index 00000000..042b5a9d --- /dev/null +++ b/app/src/main/res/layout/item_muted_domain.xml @@ -0,0 +1,41 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/account_toolbar.xml b/app/src/main/res/menu/account_toolbar.xml index 7f9d91ed..9e84f875 100644 --- a/app/src/main/res/menu/account_toolbar.xml +++ b/app/src/main/res/menu/account_toolbar.xml @@ -22,6 +22,10 @@ android:title="@string/action_block" app:showAsAction="never" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0b79709..08bcf89b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,6 +37,7 @@ Favourites Muted users Blocked users + Hidden domains Follow Requests Edit your profile Drafts @@ -92,6 +93,7 @@ Favourites Muted users Blocked users + Hidden domains Follow Requests Media Open in browser @@ -100,6 +102,7 @@ Share Mute Unmute + Mute %s Mention Hide media Open drawer @@ -142,6 +145,7 @@ Sent! User unblocked User unmuted + %s unhidden Sent! Reply sent successfully. @@ -179,6 +183,8 @@ Unfollow this account? Delete this toot? Delete and re-draft this toot? + 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. + Hide entire domain Public: Post to public timelines Unlisted: Do not show in public timelines diff --git a/app/src/main/res/xml/account_preferences.xml b/app/src/main/res/xml/account_preferences.xml index 66f3e610..1f93eaca 100644 --- a/app/src/main/res/xml/account_preferences.xml +++ b/app/src/main/res/xml/account_preferences.xml @@ -18,6 +18,10 @@ android:key="blockedUsersPreference" android:title="@string/action_view_blocks" /> + +