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:
parent
21344866d3
commit
6d6c9575c4
4 changed files with 68 additions and 1 deletions
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment;
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
@ -24,6 +25,8 @@ import android.preference.EditTextPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.support.annotation.XmlRes;
|
import android.support.annotation.XmlRes;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.BuildConfig;
|
import com.keylesspalace.tusky.BuildConfig;
|
||||||
import com.keylesspalace.tusky.PreferencesActivity;
|
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.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
|
public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
SharedPreferences sharedPreferences;
|
SharedPreferences sharedPreferences;
|
||||||
static boolean httpProxyChanged = false;
|
static boolean httpProxyChanged = false;
|
||||||
|
@ -62,6 +67,32 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre
|
||||||
|
|
||||||
addPreferencesFromResource(preference);
|
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");
|
Preference notificationPreferences = findPreference("notificationPreferences");
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,8 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -110,6 +112,8 @@ public class TimelineFragment extends SFragment implements
|
||||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||||
private boolean filterRemoveReplies;
|
private boolean filterRemoveReplies;
|
||||||
private boolean filterRemoveReblogs;
|
private boolean filterRemoveReblogs;
|
||||||
|
private boolean filterRemoveRegex;
|
||||||
|
private Matcher filterRemoveRegexMatcher;
|
||||||
private boolean hideFab;
|
private boolean hideFab;
|
||||||
private TimelineReceiver timelineReceiver;
|
private TimelineReceiver timelineReceiver;
|
||||||
private boolean topLoading;
|
private boolean topLoading;
|
||||||
|
@ -212,6 +216,10 @@ public class TimelineFragment extends SFragment implements
|
||||||
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
|
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
|
||||||
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
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);
|
timelineReceiver = new TimelineReceiver(this, this);
|
||||||
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
||||||
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(kind));
|
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(kind));
|
||||||
|
@ -497,6 +505,22 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
break;
|
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": {
|
case "alwaysShowSensitiveMedia": {
|
||||||
//it is ok if only newly loaded statuses are affected, no need to fully refresh
|
//it is ok if only newly loaded statuses are affected, no need to fully refresh
|
||||||
alwaysShowSensitiveMedia = sharedPreferences.getBoolean("alwaysShowSensitiveMedia", false);
|
alwaysShowSensitiveMedia = sharedPreferences.getBoolean("alwaysShowSensitiveMedia", false);
|
||||||
|
@ -701,7 +725,9 @@ public class TimelineFragment extends SFragment implements
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Status status = it.next();
|
Status status = it.next();
|
||||||
if ((status.getInReplyToId() != null && filterRemoveReplies)
|
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();
|
it.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_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_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_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_home">Home</string>
|
||||||
|
<string name="title_advanced">Advanced</string>
|
||||||
<string name="title_notifications">Notifications</string>
|
<string name="title_notifications">Notifications</string>
|
||||||
<string name="title_public_local">Local</string>
|
<string name="title_public_local">Local</string>
|
||||||
<string name="title_public_federated">Federated</string>
|
<string name="title_public_federated">Federated</string>
|
||||||
|
@ -177,6 +179,7 @@
|
||||||
<string name="pref_title_status_tabs">Tabs</string>
|
<string name="pref_title_status_tabs">Tabs</string>
|
||||||
<string name="pref_title_show_boosts">Show boosts</string>
|
<string name="pref_title_show_boosts">Show boosts</string>
|
||||||
<string name="pref_title_show_replies">Show replies</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_show_media_preview">Show media previews</string>
|
||||||
<string name="pref_title_proxy_settings">Proxy</string>
|
<string name="pref_title_proxy_settings">Proxy</string>
|
||||||
<string name="pref_title_http_proxy_settings">HTTP proxy</string>
|
<string name="pref_title_http_proxy_settings">HTTP proxy</string>
|
||||||
|
|
|
@ -13,4 +13,11 @@
|
||||||
android:title="@string/pref_title_show_replies" />
|
android:title="@string/pref_title_show_replies" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/title_advanced">
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="tabFilterRegex"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:title="@string/pref_title_filter_regex" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
Reference in a new issue