From 6d6c9575c41aa3706033e58d46f2cb70b5a4c9b3 Mon Sep 17 00:00:00 2001 From: Gareth Murphy Date: Thu, 5 Apr 2018 21:58:44 +0100 Subject: [PATCH] Add regular expression feed filtering (#557) * Initial implementation of regex feed filtering Mimics Mastodon web's functionality, but in a simpler form; a single regular expression is shared across the home, local and federated feeds. Strings are currently only provided in English and will need to be translated. * Fix buggy behaviour on filter regex update * Validate regex filter input Fixes buggy behaviour on inputting a regular expression feed filter by testing the expression continuously as the user types, displaying an error and disabling the 'OK' button of the dialog at any time it's not a valid regular expression. Disables spelling suggestions in the input to make the experience less frustrating and error prone. Also fixes some generally buggy behaviour upon preference change, specifically in cases where no Matcher was set prior to a new pattern being set, which would cause the app to crash. * Apply regex filter to spoiler text * Get rid of empty catch block in regex filter code * Make regex filter error string translatable --- .../tusky/fragment/PreferencesFragment.java | 31 +++++++++++++++++++ .../tusky/fragment/TimelineFragment.java | 28 ++++++++++++++++- app/src/main/res/values/strings.xml | 3 ++ .../res/xml/timeline_filter_preferences.xml | 7 +++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java index 2a2948b63..0d1530a73 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.fragment; +import android.app.AlertDialog; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; @@ -24,6 +25,8 @@ import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.support.annotation.XmlRes; +import android.text.Editable; +import android.text.TextWatcher; import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.PreferencesActivity; @@ -32,6 +35,8 @@ import com.keylesspalace.tusky.TuskyApplication; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; +import java.util.regex.Pattern; + public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences sharedPreferences; static boolean httpProxyChanged = false; @@ -62,6 +67,32 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre addPreferencesFromResource(preference); + Preference regexPref = findPreference("tabFilterRegex"); + if (regexPref != null) regexPref.setOnPreferenceClickListener(pref -> { + // Reset the error dialog when shown; if the dialog was closed with the cancel button + // while an invalid regex was present, this would otherwise cause buggy behaviour. + ((EditTextPreference) regexPref).getEditText().setError(null); + + // Test the regex as the user inputs text, ensuring immediate feedback and preventing + // setting of an invalid regex, which would cause a crash loop. + ((EditTextPreference) regexPref).getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + try { + Pattern.compile(s.toString()); + ((EditTextPreference) regexPref).getEditText().setError(null); + AlertDialog dialog = (AlertDialog) ((EditTextPreference) pref).getDialog(); + if (dialog != null) dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); + } catch (IllegalArgumentException e) { + ((AlertDialog) ((EditTextPreference) pref).getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + ((EditTextPreference) regexPref).getEditText().setError(getString(R.string.error_invalid_regex)); + } + } + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} + }); + return false; + }); Preference notificationPreferences = findPreference("notificationPreferences"); 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 422fe6fde..afb124604 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -61,6 +61,8 @@ import com.keylesspalace.tusky.viewdata.StatusViewData; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.inject.Inject; @@ -110,6 +112,8 @@ public class TimelineFragment extends SFragment implements private TabLayout.OnTabSelectedListener onTabSelectedListener; private boolean filterRemoveReplies; private boolean filterRemoveReblogs; + private boolean filterRemoveRegex; + private Matcher filterRemoveRegexMatcher; private boolean hideFab; private TimelineReceiver timelineReceiver; private boolean topLoading; @@ -212,6 +216,10 @@ public class TimelineFragment extends SFragment implements filter = preferences.getBoolean("tabFilterHomeBoosts", true); filterRemoveReblogs = kind == Kind.HOME && !filter; + String regexFilter = preferences.getString("tabFilterRegex", ""); + filterRemoveRegex = (kind == Kind.HOME || kind == Kind.PUBLIC_LOCAL || kind == Kind.PUBLIC_FEDERATED) && !regexFilter.isEmpty(); + if (filterRemoveRegex) filterRemoveRegexMatcher = Pattern.compile(regexFilter, Pattern.CASE_INSENSITIVE).matcher(""); + timelineReceiver = new TimelineReceiver(this, this); LocalBroadcastManager.getInstance(context.getApplicationContext()) .registerReceiver(timelineReceiver, TimelineReceiver.getFilter(kind)); @@ -497,6 +505,22 @@ 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(); + } + break; + } case "alwaysShowSensitiveMedia": { //it is ok if only newly loaded statuses are affected, no need to fully refresh alwaysShowSensitiveMedia = sharedPreferences.getBoolean("alwaysShowSensitiveMedia", false); @@ -701,7 +725,9 @@ public class TimelineFragment extends SFragment implements while (it.hasNext()) { Status status = it.next(); if ((status.getInReplyToId() != null && filterRemoveReplies) - || (status.getReblog() != null && filterRemoveReblogs)) { + || (status.getReblog() != null && filterRemoveReblogs) + || (filterRemoveRegex && (filterRemoveRegexMatcher.reset(status.getContent()).find() + || (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getContent()).find())))) { it.remove(); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2d282232..0739f9055 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,8 +17,10 @@ Images and videos cannot both be attached to the same status. The upload failed. At least one status must be reported. + Invalid regular expression Home + Advanced Notifications Local Federated @@ -177,6 +179,7 @@ Tabs Show boosts Show replies + Filter out by regular expressions Show media previews Proxy HTTP proxy diff --git a/app/src/main/res/xml/timeline_filter_preferences.xml b/app/src/main/res/xml/timeline_filter_preferences.xml index 93a9db135..74a8d76c2 100644 --- a/app/src/main/res/xml/timeline_filter_preferences.xml +++ b/app/src/main/res/xml/timeline_filter_preferences.xml @@ -13,4 +13,11 @@ android:title="@string/pref_title_show_replies" /> + + + + \ No newline at end of file