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
This commit is contained in:
Gareth Murphy 2018-04-05 21:58:44 +01:00 committed by Konrad Pozniak
parent 21344866d3
commit 6d6c9575c4
4 changed files with 68 additions and 1 deletions

View File

@ -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");

View File

@ -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();
}
}

View File

@ -17,8 +17,10 @@
<string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same status.</string>
<string name="error_media_upload_sending">The upload failed.</string>
<string name="error_report_too_few_statuses">At least one status must be reported.</string>
<string name="error_invalid_regex">Invalid regular expression</string>
<string name="title_home">Home</string>
<string name="title_advanced">Advanced</string>
<string name="title_notifications">Notifications</string>
<string name="title_public_local">Local</string>
<string name="title_public_federated">Federated</string>
@ -177,6 +179,7 @@
<string name="pref_title_status_tabs">Tabs</string>
<string name="pref_title_show_boosts">Show boosts</string>
<string name="pref_title_show_replies">Show replies</string>
<string name="pref_title_filter_regex">Filter out by regular expressions</string>
<string name="pref_title_show_media_preview">Show media previews</string>
<string name="pref_title_proxy_settings">Proxy</string>
<string name="pref_title_http_proxy_settings">HTTP proxy</string>

View File

@ -13,4 +13,11 @@
android:title="@string/pref_title_show_replies" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/title_advanced">
<EditTextPreference
android:key="tabFilterRegex"
android:inputType="textNoSuggestions"
android:title="@string/pref_title_filter_regex" />
</PreferenceCategory>
</PreferenceScreen>