From 934d313cb36737b8ac885e27a3144409d6502cfe Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Mon, 8 Jul 2019 12:57:53 +0200 Subject: [PATCH] Apply conversation filters to threads. Addresses #1349 (#1351) * Apply conversation filters to threads. Addresses #1349 * Cache filters for app lifetime, unless filters are modified locally * Flush cached filters when changing accounts --- .../com/keylesspalace/tusky/MainActivity.java | 2 + .../tusky/fragment/SFragment.java | 81 +++++++++++++++++++ .../tusky/fragment/TimelineFragment.java | 50 ++---------- .../tusky/fragment/ViewThreadFragment.java | 26 +++++- 4 files changed, 115 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 116570a7..28c502cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -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); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 833a1e51..d487ab41 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -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 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>() { + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> 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> 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 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; + } } 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 e48c5fa7..37bf1902 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -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>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - List 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> 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 filters, boolean refresh) { - List 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(); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 8d5796bf..b7c5e1e1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -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 ancestors, List descendants) { + private void setContext(List unfilteredAncestors, List 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 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 descendants = new ArrayList<>(); + for (Status status : unfilteredDescendants) + if (!shouldFilterStatus(status)) + descendants.add(status); + // Insert newly fetched descendants statuses.addAll(descendants); List 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(); + } }