diff --git a/mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java b/mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java deleted file mode 100644 index 87235de8fa..0000000000 --- a/mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.joinmastodon.android.utils; - -import static org.joinmastodon.android.model.FilterAction.*; -import static org.joinmastodon.android.model.FilterContext.*; -import static org.junit.Assert.*; - -import android.graphics.drawable.ColorDrawable; - -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.LegacyFilter; -import org.joinmastodon.android.model.Status; -import org.junit.Test; - -import java.time.Instant; -import java.util.EnumSet; -import java.util.List; - -public class StatusFilterPredicateTest { - - private static final LegacyFilter hideMeFilter = new LegacyFilter(), warnMeFilter = new LegacyFilter(); - private static final List allFilters = List.of(hideMeFilter, warnMeFilter); - - private static final Status - hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()), - warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now()), - noAltText = Status.ofFake(null, "display me with a warning", Instant.now()), - withAltText = Status.ofFake(null, "display me with a warning", Instant.now()); - - static { - hideMeFilter.phrase = "hide me"; - hideMeFilter.filterAction = HIDE; - hideMeFilter.context = EnumSet.of(PUBLIC, HOME); - - warnMeFilter.phrase = "warning"; - warnMeFilter.filterAction = WARN; - warnMeFilter.context = EnumSet.of(PUBLIC, HOME); - -// noAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable()); -// withAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable()); -// for (Attachment mediaAttachment : withAltText.mediaAttachments) { -// mediaAttachment.description = "Alt Text"; -// } - } - - @Test - public void testHide() { - assertFalse("should not pass because matching filter applies to given context", - new StatusFilterPredicate(allFilters, HOME).test(hideInHomePublic)); - } - - @Test - public void testHideRegardlessOfContext() { - assertTrue("filters without context should always pass", - new StatusFilterPredicate(allFilters, null).test(hideInHomePublic)); - } - - @Test - public void testHideInDifferentContext() { - assertTrue("should pass because matching filter does not apply to given context", - new StatusFilterPredicate(allFilters, THREAD).test(hideInHomePublic)); - } - - @Test - public void testHideWithWarningText() { - assertTrue("should pass because matching filter is for warnings", - new StatusFilterPredicate(allFilters, HOME).test(warnInHomePublic)); - } - - @Test - public void testWarn() { - assertFalse("should not pass because filter applies to given context", - new StatusFilterPredicate(allFilters, HOME, WARN).test(warnInHomePublic)); - } - - @Test - public void testWarnRegardlessOfContext() { - assertTrue("filters without context should always pass", - new StatusFilterPredicate(allFilters, null, WARN).test(warnInHomePublic)); - } - - @Test - public void testWarnInDifferentContext() { - assertTrue("should pass because filter does not apply to given context", - new StatusFilterPredicate(allFilters, THREAD, WARN).test(warnInHomePublic)); - } - - @Test - public void testWarnWithHideText() { - assertTrue("should pass because matching filter is for hiding", - new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic)); - } - - @Test - public void testAltTextFilterNoPass() { - assertFalse("should not pass because of no alt text", - new StatusFilterPredicate(allFilters, HOME).test(noAltText)); - } - - @Test - public void testAltTextFilterPass() { - assertTrue("should pass because of alt text", - new StatusFilterPredicate(allFilters, HOME).test(withAltText)); - } -} \ No newline at end of file diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 8f5a6fcbac..e2cd5bf91a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -21,6 +21,7 @@ import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken; import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.AltTextFilter; import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.FilterAction; import org.joinmastodon.android.model.FilterContext; @@ -37,6 +38,7 @@ import java.io.File; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -314,7 +316,7 @@ public boolean filterStatusContainingObject(T object, Function ex // Even with server-side filters, clients are expected to remove statuses that match a filter that hides them if(getLocalPreferences().serverSideFiltersSupported){ for(FilterResult filter : s.filtered){ - if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE) + if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE && filter.filter.context.contains(context)) return true; } }else if(wordFilters!=null){ @@ -326,6 +328,21 @@ public boolean filterStatusContainingObject(T object, Function ex return false; } + public List getClientSideFilters(Status status) { + List filters = List.of(); + + // filter post that have no alt text + // it only applies when activated in the settings + AltTextFilter altTextFilter=new AltTextFilter(FilterAction.WARN, EnumSet.allOf(FilterContext.class)); + if(altTextFilter.matches(status)){ + FilterResult filterResult=new FilterResult(); + filterResult.filter=altTextFilter; + filterResult.keywordMatches=List.of(); + filters.add(filterResult); + } + return filters; + } + public void updateAccountInfo(){ AccountSessionManager.getInstance().updateSessionLocalInfo(this); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/CustomLocalTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/CustomLocalTimelineFragment.java index 9fd6bcef49..d742a1451d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/CustomLocalTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/CustomLocalTimelineFragment.java @@ -7,16 +7,14 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.utils.ProvidesAssistContent; -import org.joinmastodon.android.utils.StatusFilterPredicate; import java.util.List; -import java.util.stream.Collectors; import me.grishka.appkit.api.SimpleCallback; @@ -53,7 +51,7 @@ public void onSuccess(List result){ if(!result.isEmpty()) maxID=result.get(result.size()-1).id; if (getActivity() == null) return; - result=result.stream().filter(new StatusFilterPredicate(accountID, FilterContext.PUBLIC)).collect(Collectors.toList()); + AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC); result.stream().forEach(status -> { status.account.acct += "@"+domain; status.mentions.forEach(mention -> mention.id = null); @@ -82,7 +80,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override protected FilterContext getFilterContext() { - return null; + return FilterContext.PUBLIC; } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/AltTextFilter.java b/mastodon/src/main/java/org/joinmastodon/android/model/AltTextFilter.java index 4168e65051..880f2ef62b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/AltTextFilter.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/AltTextFilter.java @@ -1,19 +1,25 @@ package org.joinmastodon.android.model; +import org.joinmastodon.android.GlobalUserPreferences; import org.jsoup.internal.StringUtil; import java.util.EnumSet; public class AltTextFilter extends LegacyFilter { - public AltTextFilter(FilterAction filterAction, FilterContext firstContext, FilterContext... restContexts) { + public AltTextFilter(FilterAction filterAction, EnumSet filterContexts) { this.filterAction = filterAction; isRemote = false; - context = EnumSet.of(firstContext, restContexts); + context = filterContexts; } @Override public boolean matches(Status status) { return status.getContentStatus().mediaAttachments.stream().map(attachment -> attachment.description).anyMatch(StringUtil::isBlank); } + + @Override + public boolean isActive(){ + return !GlobalUserPreferences.showPostsWithoutAlt; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 835eaba66a..9d5e5dbb8b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -28,10 +28,9 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.DisplayItemsParent; -import org.joinmastodon.android.model.FilterAction; -import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterResult; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.ScheduledStatus; @@ -40,7 +39,6 @@ import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.viewholders.AccountViewHolder; -import org.joinmastodon.android.utils.StatusFilterPredicate; import org.parceler.Parcels; import java.util.ArrayList; @@ -168,10 +166,6 @@ public static ArrayList buildItems(BaseStatusListFragment args.putString("account", accountID); ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus s ? s : null; - // Hide statuses that have a filter action of hide - if(!new StatusFilterPredicate(accountID, filterContext, FilterAction.HIDE).test(status)) - return new ArrayList() ; - HeaderStatusDisplayItem header=null; boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts; @@ -233,20 +227,16 @@ public static ArrayList buildItems(BaseStatusListFragment LegacyFilter applyingFilter=null; if(status.filtered!=null){ - for(FilterResult filter:status.filtered){ + List filters = status.filtered; + filters.addAll(AccountSessionManager.get(accountID).getClientSideFilters(status)); + + for(FilterResult filter:filters){ LegacyFilter f=filter.filter; if(f.isActive() && filterContext != null && f.context.contains(filterContext)){ applyingFilter=f; break; } } - - // Moshidon - if(applyingFilter==null){ - StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, FilterAction.WARN); - predicate.test(status); - applyingFilter = predicate.getApplyingFilter(); - } } ArrayList contentItems; @@ -273,8 +263,9 @@ public static ArrayList buildItems(BaseStatusListFragment boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText); if(!TextUtils.isEmpty(statusForContent.content)){ SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()); - HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered); - TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0); + if(applyingFilter!=null) + HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered); + TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, parsedText, fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0); contentItems.add(text); }else if(!hasSpoiler && header!=null){ header.needBottomPadding=true; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index 0e468d653c..cc956906f4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -321,12 +321,11 @@ public static CharSequence parseLinks(String text){ } public static void applyFilterHighlights(Context context, SpannableStringBuilder text, List filters){ - if (filters == null) return; int fgColor=UiUtils.getThemeColor(context, R.attr.colorM3Error); int bgColor=UiUtils.getThemeColor(context, R.attr.colorM3ErrorContainer); for(FilterResult filter:filters){ if(!filter.filter.isActive()) - continue;; + continue; for(String word:filter.keywordMatches){ Matcher matcher=Pattern.compile("\\b"+Pattern.quote(word)+"\\b", Pattern.CASE_INSENSITIVE).matcher(text); while(matcher.find()){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java deleted file mode 100644 index 2529dcf645..0000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.joinmastodon.android.utils; - -import static org.joinmastodon.android.model.FilterAction.HIDE; -import static org.joinmastodon.android.model.FilterAction.WARN; -import static org.joinmastodon.android.model.FilterContext.ACCOUNT; -import static org.joinmastodon.android.model.FilterContext.HOME; -import static org.joinmastodon.android.model.FilterContext.NOTIFICATIONS; -import static org.joinmastodon.android.model.FilterContext.PUBLIC; -import static org.joinmastodon.android.model.FilterContext.THREAD; - -import org.joinmastodon.android.GlobalUserPreferences; -import org.joinmastodon.android.api.session.AccountSessionManager; -import org.joinmastodon.android.model.AltTextFilter; -import org.joinmastodon.android.model.LegacyFilter; -import org.joinmastodon.android.model.FilterAction; -import org.joinmastodon.android.model.FilterContext; -import org.joinmastodon.android.model.Status; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -// TODO: This whole class has been ditched upstream. I plan to eventually refactor it to only have the still relevant clientFilters code - -public class StatusFilterPredicate implements Predicate{ - private final List clientFilters; - private final List filters; - private final FilterContext context; - private final FilterAction action; - private LegacyFilter applyingFilter; - - /** - * @param context null makes the predicate pass automatically - * @param action defines what the predicate should check: - * status should not be hidden or should not display with warning - */ - public StatusFilterPredicate(List filters, FilterContext context, FilterAction action){ - this.filters = filters; - this.context = context; - this.action = action; - this.clientFilters = getClientFilters(); - } - - public StatusFilterPredicate(List filters, FilterContext context){ - this(filters, context, HIDE); - } - - /** - * @param context null makes the predicate pass automatically - * @param action defines what the predicate should check: - * status should not be hidden or should not display with warning - */ - public StatusFilterPredicate(String accountID, FilterContext context, FilterAction action){ - filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList()); - this.context = context; - this.action = action; - this.clientFilters = getClientFilters(); - } - - private List getClientFilters() { - List filters = new ArrayList<>(); - if(!GlobalUserPreferences.showPostsWithoutAlt) { - filters.add(new AltTextFilter(WARN, HOME, PUBLIC, ACCOUNT, THREAD, NOTIFICATIONS)); - } - return filters; - } - - /** - * @param context null makes the predicate pass automatically - */ - public StatusFilterPredicate(String accountID, FilterContext context){ - this(accountID, context, HIDE); - } - - /** - * @return whether the status should be displayed without being hidden/warned about. - * will always return true if the context is null. - * true = display this status, - * false = filter this status - */ - @Override - public boolean test(Status status){ - if (context == null) return true; - - Stream matchingFilters = status.filtered != null - // use server-provided per-status info (status.filtered) if available - ? status.filtered.stream().map(f -> f.filter) - // or fall back to cached filters - : filters.stream().filter(filter -> filter.matches(status)); - - Optional applyingFilter = matchingFilters - // discard expired filters - .filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now())) - // only apply filters for given context - .filter(filter -> filter.context.contains(context)) - // treating filterAction = null (from filters list) as FilterAction.HIDE - .filter(filter -> filter.filterAction == null ? action == HIDE : filter.filterAction == action) - .findAny(); - - //Apply client filters if no server filter is triggered - if (applyingFilter.isEmpty() && !clientFilters.isEmpty()) { - applyingFilter = clientFilters.stream() - .filter(filter -> filter.context.contains(context)) - .filter(filter -> filter.filterAction == null ? action == HIDE : filter.filterAction == action) - .filter(filter -> filter.matches(status)) - .findAny(); - } - - this.applyingFilter = applyingFilter.orElse(null); - return applyingFilter.isEmpty(); - } - - public LegacyFilter getApplyingFilter() { - return applyingFilter; - } -}