generalize filtering logic

This commit is contained in:
sk 2023-05-26 02:07:50 +02:00
parent 220cd35d82
commit 99f0817bdb
17 changed files with 98 additions and 77 deletions

View File

@ -74,10 +74,8 @@ public class CacheController{
int flags=cursor.getInt(1); int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0); status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id; newMaxID=status.id;
for(Filter filter:filters){ if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
if(filter.matches(status)) continue outer;
continue outer;
}
result.add(status); result.add(status);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
String _newMaxID=newMaxID; String _newMaxID=newMaxID;
@ -92,7 +90,7 @@ public class CacheController{
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false)); callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
putHomeTimeline(result, maxID==null); putHomeTimeline(result, maxID==null);
} }
@ -148,10 +146,8 @@ public class CacheController{
ntf.postprocess(); ntf.postprocess();
newMaxID=ntf.id; newMaxID=ntf.id;
if(ntf.status!=null){ if(ntf.status!=null){
for(Filter filter:filters){ if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
if(filter.matches(ntf.status)) continue outer;
continue outer;
}
} }
result.add(ntf); result.add(ntf);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
@ -170,11 +166,7 @@ public class CacheController{
public void onSuccess(List<Notification> result){ public void onSuccess(List<Notification> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{ callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){ if(ntf.status!=null){
for(Filter filter:filters){ return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
if(filter.matches(ntf.status)){
return false;
}
}
} }
return true; return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false)); }).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));

View File

@ -60,7 +60,12 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if(getActivity()==null) return; if(getActivity()==null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList()); AccountSessionManager asm = AccountSessionManager.getInstance();
result=result.stream().filter(status -> {
// don't hide own posts in own profile
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true;
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
}).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })

View File

@ -123,7 +123,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@ -154,4 +154,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
protected void onSetFabBottomInset(int inset){ protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset; ((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@ -148,7 +148,7 @@ public class HomeTimelineFragment extends StatusListFragment {
result.get(result.size()-1).hasGapAfter=true; result.get(result.size()-1).hasGapAfter=true;
toAdd=result; toAdd=result;
} }
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME); StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList()); toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
if(!toAdd.isEmpty()){ if(!toAdd.isEmpty()){
prependItems(toAdd, true); prependItems(toAdd, true);
@ -227,7 +227,7 @@ public class HomeTimelineFragment extends StatusListFragment {
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1); List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear(); targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1); List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME); StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
for(Status s:result){ for(Status s:result){
if(idsBelowGap.contains(s.id)) if(idsBelowGap.contains(s.id))
break; break;

View File

@ -137,7 +137,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override @Override
public void onSuccess(List<Status> result) { public void onSuccess(List<Status> result) {
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })

View File

@ -79,7 +79,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) { protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true); return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null);
} }
@Override @Override

View File

@ -6,6 +6,7 @@ import android.view.View;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory; import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@ -55,7 +56,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null); List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, null);
int idx=data.indexOf(s); int idx=data.indexOf(s);
if(idx>=0){ if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault())); String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@ -156,4 +157,9 @@ public class StatusEditHistoryFragment extends StatusListFragment{
public boolean isItemEnabled(String id){ public boolean isItemEnabled(String id){
return false; return false;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return null;
}
} }

View File

@ -37,9 +37,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext()); return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext());
} }
protected Filter.FilterContext getFilterContext() { protected abstract Filter.FilterContext getFilterContext();
return Filter.FilterContext.PUBLIC;
}
@Override @Override
protected void addAccountToKnown(Status s){ protected void addAccountToKnown(Status s){

View File

@ -137,7 +137,7 @@ public class ThreadFragment extends StatusListFragment{
} }
private List<Status> filterStatuses(List<Status> statuses){ private List<Status> filterStatuses(List<Status> statuses){
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD); StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
return statuses.stream() return statuses.stream()
.filter(statusFilterPredicate) .filter(statusFilterPredicate)
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@ -26,7 +26,7 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}).exec(accountID); }).exec(accountID);
@ -42,4 +42,10 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
public boolean isOnTop() { public boolean isOnTop() {
return isRecyclerViewOnTop(list); return isRecyclerViewOnTop(list);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@ -35,7 +35,7 @@ public class FederatedTimelineFragment extends StatusListFragment {
if(!result.isEmpty()) if(!result.isEmpty())
maxID=result.get(result.size()-1).id; maxID=result.get(result.size()-1).id;
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@ -47,4 +47,9 @@ public class FederatedTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(contentWrap);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@ -34,7 +34,7 @@ public class LocalTimelineFragment extends StatusListFragment {
if(!result.isEmpty()) if(!result.isEmpty())
maxID=result.get(result.size()-1).id; maxID=result.get(result.size()-1).id;
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@ -46,4 +46,9 @@ public class LocalTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(contentWrap);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@ -15,6 +15,7 @@ import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults; import org.joinmastodon.android.model.SearchResults;
@ -80,7 +81,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
return switch(s.type){ return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)); case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)); case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null); case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null, Filter.FilterContext.PUBLIC);
}; };
} }

View File

@ -21,6 +21,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.events.FinishReportFragmentsEvent; import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
@ -261,4 +262,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
protected boolean wantsOverlaySystemNavigation(){ protected boolean wantsOverlaySystemNavigation(){
return false; return false;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return null;
}
} }

View File

@ -2,16 +2,18 @@ package org.joinmastodon.android.model;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel; import org.parceler.Parcel;
import java.time.Instant; import java.time.Instant;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream;
@Parcel @Parcel
public class Filter extends BaseModel{ public class Filter extends BaseModel{

View File

@ -81,18 +81,10 @@ public abstract class StatusDisplayItem{
}; };
} }
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, Filter.FilterContext.HOME);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){ public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext); return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
} }
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, Filter.FilterContext.HOME);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){ public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
String parentID=parentObject.getID(); String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>(); ArrayList<StatusDisplayItem> items=new ArrayList<>();
@ -102,12 +94,8 @@ public abstract class StatusDisplayItem{
args.putString("account", accountID); args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null; ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
List<Filter> filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream() if (!statusForContent.filterRevealed) {
.filter(f -> f.context.contains(filterContext)).collect(Collectors.toList()); statusForContent.filterRevealed = new StatusFilterPredicate(accountID, filterContext, Filter.FilterAction.WARN).test(status);
StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters);
if(!statusForContent.filterRevealed){
statusForContent.filterRevealed = !filterPredicate.testHasStatusWarning(status, filterContext);
} }
ReblogOrReplyLineStatusDisplayItem replyLine = null; ReblogOrReplyLineStatusDisplayItem replyLine = null;

View File

@ -8,51 +8,53 @@ import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StatusFilterPredicate implements Predicate<Status>{ public class StatusFilterPredicate implements Predicate<Status>{
private final List<Filter> filters; private final List<Filter> filters;
private final Filter.FilterContext context;
private final Filter.FilterAction action;
public StatusFilterPredicate(List<Filter> filters){ public StatusFilterPredicate(List<Filter> filters, Filter.FilterContext context){
this.filters=filters; this.filters=filters;
this.context = context;
this.action = Filter.FilterAction.HIDE;
} }
/**
* @param context null makes the predicate pass automatically
*/
public StatusFilterPredicate(String accountID, Filter.FilterContext context){ public StatusFilterPredicate(String accountID, Filter.FilterContext context){
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList()); this(accountID, context, Filter.FilterAction.HIDE);
}
/**
* @param context null makes the predicate pass automatically
* @param action defines what the predicate should check:
* should not be hidden or should not display with warning
*/
public StatusFilterPredicate(String accountID, Filter.FilterContext context, Filter.FilterAction action){
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
this.context = context;
this.action = action;
} }
// TODO: rewrite (see testHasStatusWarning) and generalize
@Override @Override
public boolean test(Status status){ public boolean test(Status status){
if(status.filtered!=null){ if (context == null) return true;
if (status.filtered.isEmpty()){
return true;
}
boolean matches=status.filtered.stream()
.map(filterResult->filterResult.filter)
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
.anyMatch(filter->filter.filterAction==Filter.FilterAction.HIDE);
return !matches;
}
for(Filter filter:filters){
if(filter.matches(status))
return false;
}
return true;
}
// TODO: move this method elsewhere; it's not part of the actual StatusFilterPredicate Stream<Filter> stream = status.filtered != null
public boolean testHasStatusWarning(Status status, Filter.FilterContext context) { // use server-provided per-status info (status.filtered) if available
if (status.filtered != null) { ? status.filtered.stream().map(f -> f.filter)
// use server-provided info on whether this status was filtered // or fall back to cached filters
if (status.filtered.isEmpty()) return false; : filters.stream().filter(filter -> filter.matches(status));
return status.filtered.stream()
.map(filterResult -> filterResult.filter) return stream
.filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now())) // discard expired filters
.filter(filter -> filter.context.contains(context)) .filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now()))
.anyMatch(filter -> filter.filterAction == Filter.FilterAction.WARN); // only apply filters for given context
} else { .filter(filter -> filter.context.contains(context))
// look through local filters instead // treating filterAction = null (from filters list) as FilterAction.HIDE
return filters.stream().anyMatch(filter -> filter.matches(status)); .noneMatch(filter -> filter.filterAction == null ? action == Filter.FilterAction.HIDE : filter.filterAction == action);
}
} }
} }