diff --git a/app/src/main/java/app/fedilab/android/activities/FilterActivity.java b/app/src/main/java/app/fedilab/android/activities/FilterActivity.java index 1aa7f883..bb12cfa0 100644 --- a/app/src/main/java/app/fedilab/android/activities/FilterActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/FilterActivity.java @@ -156,6 +156,9 @@ public class FilterActivity extends BaseActivity implements FilterAdapter.Delete popupAddFilterBinding.actionRemove.setChecked(true); } } + if (filterParams.keywords == null) { + filterParams.keywords = new ArrayList<>(); + } KeywordAdapter keywordAdapter = new KeywordAdapter(filterParams.keywords); popupAddFilterBinding.lvKeywords.setAdapter(keywordAdapter); diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonFiltersService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonFiltersService.java index 17ad69e3..88216303 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonFiltersService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonFiltersService.java @@ -46,15 +46,11 @@ public interface MastodonFiltersService { @Path("id") String id); //Add a filter - @FormUrlEncoded + @Headers({"Accept: application/json"}) @POST("filters") Call addFilter( @Header("Authorization") String token, - @Field("title") String title, - @Field("expires_in") Long expires_in, - @Field("filter_action") String filter_action, - @Field("context[]") List context, - @Field("keywords_attributes[]") List keywordsAttributes + @Body Filter.FilterParams filter ); //Edit a filter @@ -64,15 +60,6 @@ public interface MastodonFiltersService { @Header("Authorization") String token, @Path("id") String id, @Body Filter.FilterParams filter - /*@Path("id") String id, - @Field("title") String title, - @Field("expires_in") Date expires_in, - @Field("filter_action") String filter_action, - @Field("context[]") List context, - @Field("keywords_attributes[]") List keywords - @Field("keywords_attributes[][id]") List keywordId, - @Field("keywords_attributes[][keyword]") List keywords, - @Field("keywords_attributes[][whole_word]") List wholeWords*/ ); //Remove a filter diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Filter.java b/app/src/main/java/app/fedilab/android/client/entities/api/Filter.java index a52980b9..5275d346 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Filter.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Filter.java @@ -65,7 +65,7 @@ public class Filter implements Serializable { public List context; @SerializedName("whole_word") public boolean whole_word; - @SerializedName("expires_in") + @SerializedName("expires_at") public Date expires_at; @SerializedName("filter_action") public String filter_action; diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java b/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java index 89b29fba..52d5e4e7 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Notification.java @@ -39,7 +39,7 @@ public class Notification { public Status status; @SerializedName("cached") public boolean cached; - + public Filter filteredByApp; public PositionFetchMore positionFetchMore = PositionFetchMore.BOTTOM; public transient List relatedNotifications; public boolean isFetchMore; diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java index caa387a2..f996b9a4 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java @@ -92,8 +92,8 @@ public class Status implements Serializable, Cloneable { public Card card; @SerializedName("poll") public Poll poll; - /* @SerializedName("filtered") - public Filter.FilterResult filtered;*/ + @SerializedName("filtered") + public List filtered; @SerializedName("pleroma") public Pleroma pleroma; @SerializedName("cached") @@ -114,7 +114,7 @@ public class Status implements Serializable, Cloneable { public transient int cursorPosition = 0; public transient boolean submitted = false; public transient boolean spoilerChecked = false; - + public Filter filteredByApp; @Override public boolean equals(@Nullable Object obj) { boolean same = false; diff --git a/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java b/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java index f6577415..8f4942a5 100644 --- a/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java @@ -113,50 +113,40 @@ public class TimelineHelper { } if (filter.keywords != null && filter.keywords.size() > 0) { for (Filter.KeywordsAttributes filterKeyword : filter.keywords) { + String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).matches() ? "\\b" : ""; + String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).matches() ? "\\b" : ""; + Pattern p; if (filterKeyword.whole_word) { - Pattern p = Pattern.compile("(^|\\W)(" + Pattern.quote(filterKeyword.keyword) + ")($|\\W)", Pattern.CASE_INSENSITIVE); - for (Status status : statuses) { - String content; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString(); - else - content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString(); - Matcher m = p.matcher(content); - if (m.find()) { - statusesToRemove.add(status); - continue; - } - if (status.spoiler_text != null) { - String spoilerText; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); - else - spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString(); - Matcher ms = p.matcher(spoilerText); - if (ms.find()) { - statusesToRemove.add(status); - } - } - } + p = Pattern.compile(sb + "(" + Pattern.quote(filterKeyword.keyword) + ")" + eb, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); } else { - for (Status status : statuses) { - String content; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString(); - else - content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString(); - if (content.contains(filterKeyword.keyword)) { + p = Pattern.compile("#" + Pattern.quote(filterKeyword.keyword), Pattern.CASE_INSENSITIVE); + } + for (Status status : statuses) { + String content; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString(); + Matcher m = p.matcher(content); + if (m.find()) { + if (filter.filter_action.equalsIgnoreCase("warn")) { + status.filteredByApp = filter; + } else { statusesToRemove.add(status); - continue; } - - if (status.spoiler_text != null) { - String spoilerText; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); - else - spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString(); - if (spoilerText.contains(filterKeyword.keyword)) { + continue; + } + if (status.spoiler_text != null) { + String spoilerText; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); + else + spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString(); + Matcher ms = p.matcher(spoilerText); + if (ms.find()) { + if (filter.filter_action.equalsIgnoreCase("warn")) { + status.filteredByApp = filter; + } else { statusesToRemove.add(status); } } @@ -203,60 +193,46 @@ public class TimelineHelper { //Expired filter continue; } - if (!filter.context.contains("notification")) continue; if (filter.keywords != null && filter.keywords.size() > 0) { for (Filter.KeywordsAttributes filterKeyword : filter.keywords) { + String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).matches() ? "\\b" : ""; + String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).matches() ? "\\b" : ""; + Pattern p; if (filterKeyword.whole_word) { - Pattern p = Pattern.compile("(^|\\W)(" + Pattern.quote(filterKeyword.keyword) + ")($|\\W)", Pattern.CASE_INSENSITIVE); - for (Notification notification : notifications) { - if (notification.status == null) { - continue; - } - String content; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString(); - else - content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content).toString(); - Matcher m = p.matcher(content); - if (m.find()) { - notificationToRemove.add(notification); - continue; - } - if (notification.status.spoiler_text != null) { - String spoilerText; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); - else - spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text).toString(); - Matcher ms = p.matcher(spoilerText); - if (ms.find()) { - notificationToRemove.add(notification); - } - } - } + p = Pattern.compile(sb + "(" + Pattern.quote(filterKeyword.keyword) + ")" + eb, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); } else { - for (Notification notification : notifications) { - if (notification.status == null) { - continue; - } - String content; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString(); - else - content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content).toString(); - if (content.contains(filterKeyword.keyword)) { + p = Pattern.compile("#" + Pattern.quote(filterKeyword.keyword), Pattern.CASE_INSENSITIVE); + } + for (Notification notification : notifications) { + if (notification.status == null) { + continue; + } + String content; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content).toString(); + Matcher m = p.matcher(content); + if (m.find()) { + if (filter.filter_action.equalsIgnoreCase("warn")) { + notification.filteredByApp = filter; + } else { notificationToRemove.add(notification); - continue; } - - if (notification.status.spoiler_text != null) { - String spoilerText; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); - else - spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text).toString(); - if (spoilerText.contains(filterKeyword.keyword)) { + continue; + } + if (notification.status.spoiler_text != null) { + String spoilerText; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString(); + else + spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text).toString(); + Matcher ms = p.matcher(spoilerText); + if (ms.find()) { + if (filter.filter_action.equalsIgnoreCase("warn")) { + notification.filteredByApp = filter; + } else { notificationToRemove.add(notification); } } diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java index d0fce42a..58c9ea2f 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java @@ -119,6 +119,7 @@ import app.fedilab.android.client.entities.app.StatusDraft; import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.DrawerStatusArtBinding; import app.fedilab.android.databinding.DrawerStatusBinding; +import app.fedilab.android.databinding.DrawerStatusFilteredBinding; import app.fedilab.android.databinding.DrawerStatusHiddenBinding; import app.fedilab.android.databinding.DrawerStatusNotificationBinding; import app.fedilab.android.databinding.DrawerStatusReportBinding; @@ -147,6 +148,7 @@ public class StatusAdapter extends RecyclerView.Adapter public static final int STATUS_HIDDEN = 0; public static final int STATUS_VISIBLE = 1; public static final int STATUS_ART = 2; + public static final int STATUS_FILTERED = 3; private final List statusList; private final boolean minified; private final Timeline.TimeLineEnum timelineType; @@ -2102,7 +2104,12 @@ public class StatusAdapter extends RecyclerView.Adapter if (timelineType == Timeline.TimeLineEnum.ART) { return STATUS_ART; } else { - return isVisible(timelineType, statusList.get(position)) ? STATUS_VISIBLE : STATUS_HIDDEN; + if (statusList.get(position).filteredByApp != null) { + return STATUS_FILTERED; + } else { + return isVisible(timelineType, statusList.get(position)) ? STATUS_VISIBLE : STATUS_HIDDEN; + } + } } @@ -2116,6 +2123,9 @@ public class StatusAdapter extends RecyclerView.Adapter } else if (viewType == STATUS_ART) { //Art statuses DrawerStatusArtBinding itemBinding = DrawerStatusArtBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); return new StatusViewHolder(itemBinding); + } else if (viewType == STATUS_FILTERED) { //Art statuses + DrawerStatusFilteredBinding itemBinding = DrawerStatusFilteredBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); } else { //Classic statuses if (!minified) { DrawerStatusBinding itemBinding = DrawerStatusBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); @@ -2152,6 +2162,13 @@ public class StatusAdapter extends RecyclerView.Adapter StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class); SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); statusManagement(context, statusesVM, searchVM, holder, this, statusList, status, timelineType, minified, canBeFederated, fetchMoreCallBack); + } else if (viewHolder.getItemViewType() == STATUS_FILTERED) { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + holder.bindingFiltered.filteredText.setText(context.getString(R.string.filtered_by, status.filteredByApp.title)); + holder.bindingFiltered.displayButton.setOnClickListener(v -> { + status.filteredByApp = null; + notifyItemChanged(position); + }); } else if (viewHolder.getItemViewType() == STATUS_ART) { StatusViewHolder holder = (StatusViewHolder) viewHolder; MastodonHelper.loadPPMastodon(holder.bindingArt.artPp, status.account); @@ -2251,6 +2268,7 @@ public class StatusAdapter extends RecyclerView.Adapter DrawerStatusReportBinding bindingReport; DrawerStatusNotificationBinding bindingNotification; DrawerStatusArtBinding bindingArt; + DrawerStatusFilteredBinding bindingFiltered; StatusViewHolder(DrawerStatusBinding itemView) { super(itemView.getRoot()); @@ -2279,6 +2297,11 @@ public class StatusAdapter extends RecyclerView.Adapter super(itemView.getRoot()); bindingArt = itemView; } + + StatusViewHolder(DrawerStatusFilteredBinding itemView) { + super(itemView.getRoot()); + bindingFiltered = itemView; + } } diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/FiltersVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/FiltersVM.java index 2dd575c1..b85f587a 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/FiltersVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/FiltersVM.java @@ -136,7 +136,7 @@ public class FiltersVM extends AndroidViewModel { MastodonFiltersService mastodonFiltersService = initV2(instance); new Thread(() -> { Filter filter = null; - Call addFilterCall = mastodonFiltersService.addFilter(token, filterParams.title, filterParams.expires_in, filterParams.filter_action, filterParams.context, filterParams.keywords); + Call addFilterCall = mastodonFiltersService.addFilter(token, filterParams); if (addFilterCall != null) { try { Response addFiltersResponse = addFilterCall.execute(); diff --git a/app/src/main/res/layout/drawer_status_filtered.xml b/app/src/main/res/layout/drawer_status_filtered.xml new file mode 100644 index 00000000..9ab7fbba --- /dev/null +++ b/app/src/main/res/layout/drawer_status_filtered.xml @@ -0,0 +1,60 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 286166b9..8f4c4eb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2000,4 +2000,6 @@ Keyword or phrase Delete keyword Add keyword + Show aniway + Filtered: %1$s