Loading app/src/main/java/app/fedilab/android/mastodon/activities/FilterActivity.java +25 −0 Original line number Diff line number Diff line Loading @@ -44,7 +44,9 @@ import app.fedilab.android.activities.MainActivity; import app.fedilab.android.databinding.ActivityFiltersBinding; import app.fedilab.android.databinding.PopupAddFilterBinding; import app.fedilab.android.mastodon.client.entities.api.Filter; import app.fedilab.android.mastodon.client.entities.api.FilterStatus; import app.fedilab.android.mastodon.ui.drawer.FilterAdapter; import app.fedilab.android.mastodon.ui.drawer.FilteredStatusAdapter; import app.fedilab.android.mastodon.ui.drawer.KeywordAdapter; import app.fedilab.android.mastodon.viewmodel.mastodon.FiltersVM; Loading Loading @@ -140,6 +142,29 @@ public class FilterActivity extends BaseBarActivity implements FilterAdapter.Del popupAddFilterBinding.lvKeywords.setNestedScrollingEnabled(false); popupAddFilterBinding.lvKeywords.setLayoutManager(new LinearLayoutManager(context)); // Setup filtered statuses list if (filter != null && filter.statuses != null && !filter.statuses.isEmpty()) { popupAddFilterBinding.filteredStatusesContainer.setVisibility(View.VISIBLE); FilteredStatusAdapter filteredStatusAdapter = new FilteredStatusAdapter(filter.statuses, null); filteredStatusAdapter.setDeleteListener((filterStatus, position) -> { new MaterialAlertDialogBuilder(context) .setTitle(R.string.delete) .setMessage(filterStatus.status_id) .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) .setPositiveButton(R.string.delete, (dialog, which) -> { filtersVM.removeStatusFromFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filterStatus.id); filteredStatusAdapter.removeItem(position); if (filter.statuses.isEmpty()) { popupAddFilterBinding.filteredStatusesContainer.setVisibility(View.GONE); } }) .show(); }); popupAddFilterBinding.lvFilteredStatuses.setAdapter(filteredStatusAdapter); popupAddFilterBinding.lvFilteredStatuses.setNestedScrollingEnabled(false); popupAddFilterBinding.lvFilteredStatuses.setLayoutManager(new LinearLayoutManager(context)); } popupAddFilterBinding.addKeyword.setOnClickListener(v -> { Filter.KeywordsParams keywordsParams = new Filter.KeywordsParams(); keywordsParams.whole_word = true; Loading app/src/main/java/app/fedilab/android/mastodon/client/endpoints/MastodonFiltersService.java +23 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package app.fedilab.android.mastodon.client.endpoints; import java.util.List; import app.fedilab.android.mastodon.client.entities.api.Filter; import app.fedilab.android.mastodon.client.entities.api.FilterStatus; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; Loading Loading @@ -102,5 +103,27 @@ public interface MastodonFiltersService { @Path("id") String id ); //Get statuses for a filter @GET("filters/{filter_id}/statuses") Call<List<FilterStatus>> getStatusFilter( @Header("Authorization") String token, @Path("filter_id") String filter_id ); //Add a status to a filter @FormUrlEncoded @POST("filters/{filter_id}/statuses") Call<FilterStatus> addStatusFilter( @Header("Authorization") String token, @Path("filter_id") String filter_id, @Field("status_id") String status_id ); //Remove a status from a filter @DELETE("filters/statuses/{id}") Call<Void> removeStatusFilter( @Header("Authorization") String token, @Path("id") String id ); } app/src/main/java/app/fedilab/android/mastodon/client/entities/api/Filter.java +2 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,8 @@ public class Filter implements Serializable { public String filter_action; @SerializedName("keywords") public List<KeywordsAttributes> keywords; @SerializedName("statuses") public List<FilterStatus> statuses; public static String getValueOf(FilterParams filterParams) { Gson gson = new Gson(); Loading app/src/main/java/app/fedilab/android/mastodon/client/entities/api/FilterStatus.java 0 → 100644 +26 −0 Original line number Diff line number Diff line package app.fedilab.android.mastodon.client.entities.api; /* Copyright 2026 Thomas Schneider * * This file is a part of Fedilab * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see <http://www.gnu.org/licenses>. */ import com.google.gson.annotations.SerializedName; import java.io.Serializable; public class FilterStatus implements Serializable { @SerializedName("id") public String id; @SerializedName("status_id") public String status_id; } app/src/main/java/app/fedilab/android/mastodon/helper/TimelineHelper.java +47 −28 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import app.fedilab.android.mastodon.client.endpoints.MastodonFiltersService; import app.fedilab.android.mastodon.client.entities.api.Account; import app.fedilab.android.mastodon.client.entities.api.Attachment; import app.fedilab.android.mastodon.client.entities.api.Filter; import app.fedilab.android.mastodon.client.entities.api.FilterStatus; import app.fedilab.android.mastodon.client.entities.api.Notification; import app.fedilab.android.mastodon.client.entities.api.Status; import app.fedilab.android.mastodon.client.entities.app.Timeline; Loading Loading @@ -123,12 +124,15 @@ public class TimelineHelper { contextMatches = filter.context.contains("public"); } if (!contextMatches || filter.keywords == null || filter.keywords.isEmpty()) { boolean hasKeywords = filter.keywords != null && !filter.keywords.isEmpty(); boolean hasStatuses = filter.statuses != null && !filter.statuses.isEmpty(); if (!contextMatches || (!hasKeywords && !hasStatuses)) { continue; } // Precompile patterns for this filter List<Pattern> patterns = new ArrayList<>(); if (hasKeywords) { for (Filter.KeywordsAttributes filterKeyword : filter.keywords) { String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).find() ? "\\b" : ""; String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).find() ? "\\b" : ""; Loading @@ -140,6 +144,7 @@ public class TimelineHelper { } patterns.add(p); } } compiledFilters.add(new CompiledFilter(filter, patterns)); } } Loading @@ -160,6 +165,19 @@ public class TimelineHelper { for (CompiledFilter compiledFilter : compiledFilters) { boolean matched = false; // Check if status ID is in filter's statuses list if (compiledFilter.filter.statuses != null) { String statusIdToCheck = status.reblog != null ? status.reblog.id : status.id; for (FilterStatus filterStatus : compiledFilter.filter.statuses) { if (filterStatus.status_id != null && filterStatus.status_id.equals(statusIdToCheck)) { matched = true; break; } } } // Check keywords (content, spoiler, media) if not already matched by status ID if (!matched) { // Check content for (Pattern pattern : compiledFilter.patterns) { if (pattern.matcher(content).find()) { Loading Loading @@ -192,6 +210,7 @@ public class TimelineHelper { } } } } if (matched) { status.filteredByApp = compiledFilter.filter; Loading Loading
app/src/main/java/app/fedilab/android/mastodon/activities/FilterActivity.java +25 −0 Original line number Diff line number Diff line Loading @@ -44,7 +44,9 @@ import app.fedilab.android.activities.MainActivity; import app.fedilab.android.databinding.ActivityFiltersBinding; import app.fedilab.android.databinding.PopupAddFilterBinding; import app.fedilab.android.mastodon.client.entities.api.Filter; import app.fedilab.android.mastodon.client.entities.api.FilterStatus; import app.fedilab.android.mastodon.ui.drawer.FilterAdapter; import app.fedilab.android.mastodon.ui.drawer.FilteredStatusAdapter; import app.fedilab.android.mastodon.ui.drawer.KeywordAdapter; import app.fedilab.android.mastodon.viewmodel.mastodon.FiltersVM; Loading Loading @@ -140,6 +142,29 @@ public class FilterActivity extends BaseBarActivity implements FilterAdapter.Del popupAddFilterBinding.lvKeywords.setNestedScrollingEnabled(false); popupAddFilterBinding.lvKeywords.setLayoutManager(new LinearLayoutManager(context)); // Setup filtered statuses list if (filter != null && filter.statuses != null && !filter.statuses.isEmpty()) { popupAddFilterBinding.filteredStatusesContainer.setVisibility(View.VISIBLE); FilteredStatusAdapter filteredStatusAdapter = new FilteredStatusAdapter(filter.statuses, null); filteredStatusAdapter.setDeleteListener((filterStatus, position) -> { new MaterialAlertDialogBuilder(context) .setTitle(R.string.delete) .setMessage(filterStatus.status_id) .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) .setPositiveButton(R.string.delete, (dialog, which) -> { filtersVM.removeStatusFromFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filterStatus.id); filteredStatusAdapter.removeItem(position); if (filter.statuses.isEmpty()) { popupAddFilterBinding.filteredStatusesContainer.setVisibility(View.GONE); } }) .show(); }); popupAddFilterBinding.lvFilteredStatuses.setAdapter(filteredStatusAdapter); popupAddFilterBinding.lvFilteredStatuses.setNestedScrollingEnabled(false); popupAddFilterBinding.lvFilteredStatuses.setLayoutManager(new LinearLayoutManager(context)); } popupAddFilterBinding.addKeyword.setOnClickListener(v -> { Filter.KeywordsParams keywordsParams = new Filter.KeywordsParams(); keywordsParams.whole_word = true; Loading
app/src/main/java/app/fedilab/android/mastodon/client/endpoints/MastodonFiltersService.java +23 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package app.fedilab.android.mastodon.client.endpoints; import java.util.List; import app.fedilab.android.mastodon.client.entities.api.Filter; import app.fedilab.android.mastodon.client.entities.api.FilterStatus; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; Loading Loading @@ -102,5 +103,27 @@ public interface MastodonFiltersService { @Path("id") String id ); //Get statuses for a filter @GET("filters/{filter_id}/statuses") Call<List<FilterStatus>> getStatusFilter( @Header("Authorization") String token, @Path("filter_id") String filter_id ); //Add a status to a filter @FormUrlEncoded @POST("filters/{filter_id}/statuses") Call<FilterStatus> addStatusFilter( @Header("Authorization") String token, @Path("filter_id") String filter_id, @Field("status_id") String status_id ); //Remove a status from a filter @DELETE("filters/statuses/{id}") Call<Void> removeStatusFilter( @Header("Authorization") String token, @Path("id") String id ); }
app/src/main/java/app/fedilab/android/mastodon/client/entities/api/Filter.java +2 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,8 @@ public class Filter implements Serializable { public String filter_action; @SerializedName("keywords") public List<KeywordsAttributes> keywords; @SerializedName("statuses") public List<FilterStatus> statuses; public static String getValueOf(FilterParams filterParams) { Gson gson = new Gson(); Loading
app/src/main/java/app/fedilab/android/mastodon/client/entities/api/FilterStatus.java 0 → 100644 +26 −0 Original line number Diff line number Diff line package app.fedilab.android.mastodon.client.entities.api; /* Copyright 2026 Thomas Schneider * * This file is a part of Fedilab * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see <http://www.gnu.org/licenses>. */ import com.google.gson.annotations.SerializedName; import java.io.Serializable; public class FilterStatus implements Serializable { @SerializedName("id") public String id; @SerializedName("status_id") public String status_id; }
app/src/main/java/app/fedilab/android/mastodon/helper/TimelineHelper.java +47 −28 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import app.fedilab.android.mastodon.client.endpoints.MastodonFiltersService; import app.fedilab.android.mastodon.client.entities.api.Account; import app.fedilab.android.mastodon.client.entities.api.Attachment; import app.fedilab.android.mastodon.client.entities.api.Filter; import app.fedilab.android.mastodon.client.entities.api.FilterStatus; import app.fedilab.android.mastodon.client.entities.api.Notification; import app.fedilab.android.mastodon.client.entities.api.Status; import app.fedilab.android.mastodon.client.entities.app.Timeline; Loading Loading @@ -123,12 +124,15 @@ public class TimelineHelper { contextMatches = filter.context.contains("public"); } if (!contextMatches || filter.keywords == null || filter.keywords.isEmpty()) { boolean hasKeywords = filter.keywords != null && !filter.keywords.isEmpty(); boolean hasStatuses = filter.statuses != null && !filter.statuses.isEmpty(); if (!contextMatches || (!hasKeywords && !hasStatuses)) { continue; } // Precompile patterns for this filter List<Pattern> patterns = new ArrayList<>(); if (hasKeywords) { for (Filter.KeywordsAttributes filterKeyword : filter.keywords) { String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).find() ? "\\b" : ""; String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).find() ? "\\b" : ""; Loading @@ -140,6 +144,7 @@ public class TimelineHelper { } patterns.add(p); } } compiledFilters.add(new CompiledFilter(filter, patterns)); } } Loading @@ -160,6 +165,19 @@ public class TimelineHelper { for (CompiledFilter compiledFilter : compiledFilters) { boolean matched = false; // Check if status ID is in filter's statuses list if (compiledFilter.filter.statuses != null) { String statusIdToCheck = status.reblog != null ? status.reblog.id : status.id; for (FilterStatus filterStatus : compiledFilter.filter.statuses) { if (filterStatus.status_id != null && filterStatus.status_id.equals(statusIdToCheck)) { matched = true; break; } } } // Check keywords (content, spoiler, media) if not already matched by status ID if (!matched) { // Check content for (Pattern pattern : compiledFilter.patterns) { if (pattern.matcher(content).find()) { Loading Loading @@ -192,6 +210,7 @@ public class TimelineHelper { } } } } if (matched) { status.filteredByApp = compiledFilter.filter; Loading