Share filters with web client (#956)
* First step toward synchronized content filters * Add simple filter management UI * Remove old regex filter UI * More cleanup * Escape filter phrases when applying them via regex * Apply code review feedback * Fix live timeline update when filters change
This commit is contained in:
parent
463b008090
commit
5135daad2c
13 changed files with 439 additions and 91 deletions
|
@ -20,6 +20,7 @@ import android.content.Intent;
|
|||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -45,6 +46,7 @@ import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
|||
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Filter;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
|
@ -64,7 +66,9 @@ import com.keylesspalace.tusky.view.BackgroundMessageView;
|
|||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -312,6 +316,20 @@ public class TimelineFragment extends SFragment implements
|
|||
});
|
||||
}
|
||||
|
||||
private void reloadFilters(boolean refresh) {
|
||||
mastodonApi.getFilters().enqueue(new Callback<List<Filter>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Filter>> call, Response<List<Filter>> response) {
|
||||
applyFilters(response.body(), refresh);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Filter>> call, Throwable t) {
|
||||
Log.e(TAG, "Error getting filters from server");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupTimelinePreferences() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
||||
|
@ -325,16 +343,43 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
|
||||
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
||||
reloadFilters(false);
|
||||
}
|
||||
|
||||
String regexFilter = preferences.getString("tabFilterRegex", "");
|
||||
filterRemoveRegex = (kind == Kind.HOME
|
||||
|| kind == Kind.PUBLIC_LOCAL
|
||||
|| kind == Kind.PUBLIC_FEDERATED)
|
||||
&& !regexFilter.isEmpty();
|
||||
private static boolean filterContextMatchesKind(Kind kind, List<String> filterContext) {
|
||||
// home, notifications, public, thread
|
||||
switch(kind) {
|
||||
case HOME:
|
||||
return filterContext.contains(Filter.HOME);
|
||||
case PUBLIC_FEDERATED:
|
||||
case PUBLIC_LOCAL:
|
||||
case TAG:
|
||||
return filterContext.contains(Filter.PUBLIC);
|
||||
case FAVOURITES:
|
||||
return (filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String filterToRegexToken(Filter filter) {
|
||||
String phrase = Pattern.quote(filter.getPhrase());
|
||||
return filter.getWholeWord() ? String.format("\\b%s\\b", phrase) : phrase;
|
||||
}
|
||||
|
||||
private void applyFilters(List<Filter> filters, boolean refresh) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
for (Filter filter : filters) {
|
||||
if (filterContextMatchesKind(kind, filter.getContext())) {
|
||||
tokens.add(filterToRegexToken(filter));
|
||||
}
|
||||
}
|
||||
filterRemoveRegex = !tokens.isEmpty();
|
||||
if (filterRemoveRegex) {
|
||||
filterRemoveRegexMatcher = Pattern.compile(regexFilter, Pattern.CASE_INSENSITIVE)
|
||||
.matcher("");
|
||||
filterRemoveRegexMatcher = Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher("");
|
||||
}
|
||||
if (refresh) {
|
||||
fullyRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -765,19 +810,12 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
break;
|
||||
}
|
||||
case "tabFilterRegex": {
|
||||
boolean oldFilterRemoveRegex = filterRemoveRegex;
|
||||
String newFilterRemoveRegexPattern = sharedPreferences.getString("tabFilterRegex", "");
|
||||
boolean patternChanged;
|
||||
if (filterRemoveRegexMatcher != null) {
|
||||
patternChanged = !newFilterRemoveRegexPattern.equalsIgnoreCase(filterRemoveRegexMatcher.pattern().pattern());
|
||||
} else {
|
||||
patternChanged = !newFilterRemoveRegexPattern.isEmpty();
|
||||
}
|
||||
filterRemoveRegex = (kind == Kind.HOME || kind == Kind.PUBLIC_LOCAL || kind == Kind.PUBLIC_FEDERATED) && !newFilterRemoveRegexPattern.isEmpty();
|
||||
if (oldFilterRemoveRegex != filterRemoveRegex || patternChanged) {
|
||||
filterRemoveRegexMatcher = Pattern.compile(newFilterRemoveRegexPattern, Pattern.CASE_INSENSITIVE).matcher("");
|
||||
fullyRefresh();
|
||||
case Filter.HOME:
|
||||
case Filter.NOTIFICATIONS:
|
||||
case Filter.THREAD:
|
||||
case Filter.PUBLIC: {
|
||||
if (filterContextMatchesKind(kind, Collections.singletonList(key))) {
|
||||
reloadFilters(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
|||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
|
@ -65,6 +66,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
private lateinit var defaultMediaSensitivityPreference: SwitchPreference
|
||||
private lateinit var alwaysShowSensitiveMediaPreference: SwitchPreference
|
||||
private lateinit var mediaPreviewEnabledPreference: SwitchPreference
|
||||
private lateinit var homeFiltersPreference: Preference
|
||||
private lateinit var notificationFiltersPreference: Preference
|
||||
private lateinit var publicFiltersPreference: Preference
|
||||
private lateinit var threadFiltersPreference: Preference
|
||||
|
||||
private val iconSize by lazy {resources.getDimensionPixelSize(R.dimen.preference_icon_size)}
|
||||
|
||||
|
@ -79,6 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
defaultMediaSensitivityPreference = findPreference("defaultMediaSensitivity") as SwitchPreference
|
||||
mediaPreviewEnabledPreference = findPreference("mediaPreviewEnabled") as SwitchPreference
|
||||
alwaysShowSensitiveMediaPreference = findPreference("alwaysShowSensitiveMedia") as SwitchPreference
|
||||
homeFiltersPreference = findPreference("homeFilters")
|
||||
notificationFiltersPreference = findPreference("notificationFilters")
|
||||
publicFiltersPreference = findPreference("publicFilters")
|
||||
threadFiltersPreference = findPreference("threadFilters")
|
||||
|
||||
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)
|
||||
|
@ -88,12 +97,15 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
tabPreference.onPreferenceClickListener = this
|
||||
mutedUsersPreference.onPreferenceClickListener = this
|
||||
blockedUsersPreference.onPreferenceClickListener = this
|
||||
homeFiltersPreference.onPreferenceClickListener = this
|
||||
notificationFiltersPreference.onPreferenceClickListener = this
|
||||
publicFiltersPreference.onPreferenceClickListener = this
|
||||
threadFiltersPreference.onPreferenceClickListener = this
|
||||
|
||||
defaultPostPrivacyPreference.onPreferenceChangeListener = this
|
||||
defaultMediaSensitivityPreference.onPreferenceChangeListener = this
|
||||
mediaPreviewEnabledPreference.onPreferenceChangeListener = this
|
||||
alwaysShowSensitiveMediaPreference.onPreferenceChangeListener = this
|
||||
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -144,7 +156,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
|
||||
override fun onPreferenceClick(preference: Preference): Boolean {
|
||||
|
||||
when(preference) {
|
||||
return when(preference) {
|
||||
notificationPreference -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val intent = Intent()
|
||||
|
@ -159,30 +171,42 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
}
|
||||
|
||||
}
|
||||
return true
|
||||
true
|
||||
}
|
||||
tabPreference -> {
|
||||
val intent = Intent(context, TabPreferenceActivity::class.java)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
return true
|
||||
true
|
||||
}
|
||||
mutedUsersPreference -> {
|
||||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
return true
|
||||
true
|
||||
}
|
||||
blockedUsersPreference -> {
|
||||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
return true
|
||||
true
|
||||
}
|
||||
homeFiltersPreference -> {
|
||||
launchFilterActivity(Filter.HOME, R.string.title_home)
|
||||
}
|
||||
notificationFiltersPreference -> {
|
||||
launchFilterActivity(Filter.NOTIFICATIONS, R.string.title_notifications)
|
||||
}
|
||||
publicFiltersPreference -> {
|
||||
launchFilterActivity(Filter.PUBLIC, R.string.pref_title_public_filter_keywords)
|
||||
}
|
||||
threadFiltersPreference -> {
|
||||
launchFilterActivity(Filter.THREAD, R.string.pref_title_thread_filter_keywords)
|
||||
}
|
||||
|
||||
else -> return false
|
||||
else -> false
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -249,6 +273,15 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
return drawable
|
||||
}
|
||||
|
||||
fun launchFilterActivity(filterContext: String, titleResource: Int): Boolean {
|
||||
val intent = Intent(context, FiltersActivity::class.java)
|
||||
intent.putExtra(FiltersActivity.FILTERS_CONTEXT, filterContext)
|
||||
intent.putExtra(FiltersActivity.FILTERS_TITLE, getString(titleResource))
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(): AccountPreferencesFragment {
|
||||
return AccountPreferencesFragment()
|
||||
|
|
|
@ -17,14 +17,8 @@ package com.keylesspalace.tusky.fragment.preference
|
|||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.EditText
|
||||
import androidx.preference.Preference
|
||||
import com.keylesspalace.tusky.R
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
|
@ -32,50 +26,7 @@ class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
|||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.timeline_filter_preferences)
|
||||
|
||||
sharedPreferences = preferenceManager.sharedPreferences
|
||||
|
||||
val regexPref: Preference = findPreference("tabFilterRegex")
|
||||
|
||||
regexPref.summary = sharedPreferences.getString("tabFilterRegex", "")
|
||||
regexPref.setOnPreferenceClickListener {
|
||||
|
||||
val editText = EditText(requireContext())
|
||||
editText.setText(sharedPreferences.getString("tabFilterRegex", ""))
|
||||
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.pref_title_filter_regex)
|
||||
.setView(editText)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString("tabFilterRegex", editText.text.toString())
|
||||
.apply()
|
||||
regexPref.summary = editText.text.toString()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
|
||||
editText.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(newRegex: Editable) {
|
||||
try {
|
||||
Pattern.compile(newRegex.toString())
|
||||
editText.error = null
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
editText.error = getString(R.string.error_invalid_regex)
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s1: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s1: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
dialog.show()
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue