* Apply conversation filters to threads. Addresses #1349 * Cache filters for app lifetime, unless filters are modified locally * Flush cached filters when changing accounts
This commit is contained in:
parent
578b816a8e
commit
d4ec0bb101
4 changed files with 115 additions and 44 deletions
|
@ -49,6 +49,7 @@ import com.keylesspalace.tusky.appstore.ProfileEditedEvent;
|
|||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.fragment.SFragment;
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
|
||||
import com.keylesspalace.tusky.pager.MainPagerAdapter;
|
||||
|
@ -492,6 +493,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
|
||||
private void changeAccount(long newSelectedId, @Nullable Intent forward) {
|
||||
cacheUpdater.stop();
|
||||
SFragment.flushFilters();
|
||||
accountManager.setActiveAccount(newSelectedId);
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
|
|
|
@ -26,7 +26,9 @@ import android.net.Uri;
|
|||
import android.os.Environment;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
@ -51,17 +53,25 @@ import com.keylesspalace.tusky.db.AccountEntity;
|
|||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Filter;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.network.TimelineCases;
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
|
||||
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
|
||||
* of that is complicated by how they're coupled with Status and Notification and the corresponding
|
||||
|
@ -76,6 +86,10 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
|
||||
private BottomSheetActivity bottomSheetActivity;
|
||||
|
||||
private static List<Filter> filters;
|
||||
private boolean filterRemoveRegex;
|
||||
private Matcher filterRemoveRegexMatcher;
|
||||
|
||||
@Inject
|
||||
public MastodonApi mastodonApi;
|
||||
@Inject
|
||||
|
@ -83,6 +97,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
@Inject
|
||||
public TimelineCases timelineCases;
|
||||
|
||||
private static final String TAG = "SFragment";
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
super.startActivity(intent);
|
||||
|
@ -418,4 +434,69 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
void reloadFilters(boolean forceRefresh) {
|
||||
if (filters != null && !forceRefresh) {
|
||||
applyFilters(forceRefresh);
|
||||
return;
|
||||
}
|
||||
|
||||
mastodonApi.getFilters().enqueue(new Callback<List<Filter>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<Filter>> call, @NonNull Response<List<Filter>> response) {
|
||||
filters = response.body();
|
||||
if (response.isSuccessful() && filters != null) {
|
||||
applyFilters(forceRefresh);
|
||||
} else {
|
||||
Log.e(TAG, "Error getting filters from server");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<Filter>> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Error getting filters from server", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected boolean filterIsRelevant(Filter filter) {
|
||||
// Called when building local filter expression
|
||||
// Override to select relevant filters for your fragment
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void refreshAfterApplyingFilters() {
|
||||
// Called after filters are updated
|
||||
// Override to refresh your fragment
|
||||
}
|
||||
|
||||
boolean shouldFilterStatus(Status status) {
|
||||
return (filterRemoveRegex && (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find()
|
||||
|| (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find())));
|
||||
}
|
||||
|
||||
private void applyFilters(boolean refresh) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
for (Filter filter : filters) {
|
||||
if (filterIsRelevant(filter)) {
|
||||
tokens.add(filterToRegexToken(filter));
|
||||
}
|
||||
}
|
||||
filterRemoveRegex = !tokens.isEmpty();
|
||||
if (filterRemoveRegex) {
|
||||
filterRemoveRegexMatcher = Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher("");
|
||||
}
|
||||
if (refresh) {
|
||||
refreshAfterApplyingFilters();
|
||||
}
|
||||
}
|
||||
|
||||
private static String filterToRegexToken(Filter filter) {
|
||||
String phrase = Pattern.quote(filter.getPhrase());
|
||||
return filter.getWholeWord() ? String.format("(^|\\W)%s($|\\W)", phrase) : phrase;
|
||||
}
|
||||
|
||||
public static void flushFilters() {
|
||||
filters = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ 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;
|
||||
|
@ -76,8 +75,6 @@ import java.util.List;
|
|||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
|
@ -164,8 +161,6 @@ public class TimelineFragment extends SFragment implements
|
|||
private EndlessOnScrollListener scrollListener;
|
||||
private boolean filterRemoveReplies;
|
||||
private boolean filterRemoveReblogs;
|
||||
private boolean filterRemoveRegex;
|
||||
private Matcher filterRemoveRegexMatcher;
|
||||
private boolean hideFab;
|
||||
private boolean bottomLoading;
|
||||
|
||||
|
@ -341,25 +336,6 @@ public class TimelineFragment extends SFragment implements
|
|||
});
|
||||
}
|
||||
|
||||
private void reloadFilters(boolean refresh) {
|
||||
mastodonApi.getFilters().enqueue(new Callback<List<Filter>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<Filter>> call, @NonNull Response<List<Filter>> response) {
|
||||
List<Filter> filterList = response.body();
|
||||
if (response.isSuccessful() && filterList != null) {
|
||||
applyFilters(filterList, refresh);
|
||||
} else {
|
||||
Log.e(TAG, "Error getting filters from server");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<Filter>> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Error getting filters from server", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupTimelinePreferences() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
||||
|
@ -396,25 +372,14 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private static String filterToRegexToken(Filter filter) {
|
||||
String phrase = Pattern.quote(filter.getPhrase());
|
||||
return filter.getWholeWord() ? String.format("(^|\\W)%s($|\\W)", phrase) : phrase;
|
||||
@Override
|
||||
protected boolean filterIsRelevant(Filter filter) {
|
||||
return filterContextMatchesKind(kind, filter.getContext());
|
||||
}
|
||||
|
||||
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(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher("");
|
||||
}
|
||||
if (refresh) {
|
||||
fullyRefresh();
|
||||
}
|
||||
@Override
|
||||
protected void refreshAfterApplyingFilters() {
|
||||
fullyRefresh();
|
||||
}
|
||||
|
||||
private void setupSwipeRefreshLayout() {
|
||||
|
@ -1142,8 +1107,7 @@ public class TimelineFragment extends SFragment implements
|
|||
if (status != null
|
||||
&& ((status.getInReplyToId() != null && filterRemoveReplies)
|
||||
|| (status.getReblog() != null && filterRemoveReblogs)
|
||||
|| (filterRemoveRegex && (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find()
|
||||
|| (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find()))))) {
|
||||
|| shouldFilterStatus(status))) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import com.keylesspalace.tusky.appstore.ReblogEvent;
|
|||
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Filter;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.StatusContext;
|
||||
|
@ -53,6 +54,7 @@ import com.keylesspalace.tusky.util.ViewDataUtils;
|
|||
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -156,6 +158,7 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
adapter.setAnimateAvatar(animateAvatars);
|
||||
boolean showBotIndicator = preferences.getBoolean("showBotOverlay", true);
|
||||
adapter.setShowBotOverlay(showBotIndicator);
|
||||
reloadFilters(false);
|
||||
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
|
@ -511,7 +514,7 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
return i;
|
||||
}
|
||||
|
||||
private void setContext(List<Status> ancestors, List<Status> descendants) {
|
||||
private void setContext(List<Status> unfilteredAncestors, List<Status> unfilteredDescendants) {
|
||||
Status mainStatus = null;
|
||||
|
||||
// In case of refresh, remove old ancestors and descendants first. We'll remove all blindly,
|
||||
|
@ -523,6 +526,11 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
adapter.clearItems();
|
||||
}
|
||||
|
||||
ArrayList<Status> ancestors = new ArrayList<>();
|
||||
for (Status status : unfilteredAncestors)
|
||||
if (!shouldFilterStatus(status))
|
||||
ancestors.add(status);
|
||||
|
||||
// Insert newly fetched ancestors
|
||||
statusIndex = ancestors.size();
|
||||
adapter.setDetailedStatusPosition(statusIndex);
|
||||
|
@ -541,12 +549,18 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
if (mainStatus != null) {
|
||||
// In case we needed to delete everything (which is way easier than deleting
|
||||
// everything except one), re-insert the remaining status here.
|
||||
// Not filtering the main status, since the user explicitly chose to be here
|
||||
statuses.add(statusIndex, mainStatus);
|
||||
StatusViewData.Concrete viewData = statuses.getPairedItem(statusIndex);
|
||||
|
||||
adapter.addItem(statusIndex, viewData);
|
||||
}
|
||||
|
||||
ArrayList<Status> descendants = new ArrayList<>();
|
||||
for (Status status : unfilteredDescendants)
|
||||
if (!shouldFilterStatus(status))
|
||||
descendants.add(status);
|
||||
|
||||
// Insert newly fetched descendants
|
||||
statuses.addAll(descendants);
|
||||
List<StatusViewData.Concrete> descendantsViewData;
|
||||
|
@ -671,4 +685,14 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
activity.setRevealButtonState(allExpanded() ? ViewThreadActivity.REVEAL_BUTTON_HIDE :
|
||||
ViewThreadActivity.REVEAL_BUTTON_REVEAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean filterIsRelevant(Filter filter) {
|
||||
return filter.getContext().contains(Filter.THREAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshAfterApplyingFilters() {
|
||||
onRefresh();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue