Filtered posts in timelines (AND-8)
This commit is contained in:
parent
a24b4363d7
commit
17957b69d1
|
@ -9,7 +9,7 @@ android {
|
|||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 57
|
||||
versionCode 58
|
||||
versionName "1.3.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
|
|
|
@ -16,19 +16,16 @@ import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
|||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
@ -60,7 +57,6 @@ public class CacheController{
|
|||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
|
@ -68,20 +64,16 @@ public class CacheController{
|
|||
ArrayList<Status> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||
status.postprocess();
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(status))
|
||||
continue outer;
|
||||
}
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
return;
|
||||
}
|
||||
|
@ -93,7 +85,9 @@ public class CacheController{
|
|||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
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));
|
||||
ArrayList<Status> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
|
@ -140,7 +134,6 @@ public class CacheController{
|
|||
}
|
||||
return;
|
||||
}
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
|
@ -148,20 +141,14 @@ public class CacheController{
|
|||
ArrayList<Notification> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
||||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
result.add(ntf);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
||||
return;
|
||||
}
|
||||
|
@ -175,16 +162,9 @@ public class CacheController{
|
|||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(result.stream().filter(ntf->{
|
||||
if(ntf.status!=null){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(ntf.status)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||
callback.onSuccess(res);
|
||||
putNotifications(result, onlyMentions, maxID==null);
|
||||
if(!onlyMentions){
|
||||
|
|
|
@ -9,6 +9,7 @@ public class AccountLocalPreferences{
|
|||
public boolean customEmojiInNames;
|
||||
public boolean showCWs;
|
||||
public boolean hideSensitiveMedia;
|
||||
public boolean serverSideFiltersSupported;
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs){
|
||||
this.prefs=prefs;
|
||||
|
@ -16,6 +17,7 @@ public class AccountLocalPreferences{
|
|||
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
|
||||
showCWs=prefs.getBoolean("showCWs", true);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||
}
|
||||
|
||||
public long getNotificationsPauseEndTime(){
|
||||
|
@ -32,6 +34,7 @@ public class AccountLocalPreferences{
|
|||
.putBoolean("emojiInNames", customEmojiInNames)
|
||||
.putBoolean("showCWs", showCWs)
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,13 @@ 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.Application;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
@ -31,6 +35,7 @@ import org.joinmastodon.android.utils.ObjectIdComparator;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
@ -213,4 +218,47 @@ public class AccountSession{
|
|||
localPreferences=new AccountLocalPreferences(getRawLocalPreferences());
|
||||
return localPreferences;
|
||||
}
|
||||
|
||||
public void filterStatuses(List<Status> statuses, FilterContext context){
|
||||
filterStatusContainingObjects(statuses, Function.identity(), context);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context){
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
if(s.filtered==null)
|
||||
return false;
|
||||
for(FilterResult filter:s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(wordFilters==null)
|
||||
return;
|
||||
for(T obj:objects){
|
||||
Status s=extractor.apply(obj);
|
||||
if(s!=null && s.filtered!=null){
|
||||
getLocalPreferences().serverSideFiltersSupported=true;
|
||||
getLocalPreferences().save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
for(LegacyFilter filter:wordFilters){
|
||||
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -260,7 +260,7 @@ public class AccountSessionManager{
|
|||
if(now-session.infoLastUpdated>24L*3600_000L){
|
||||
updateSessionLocalInfo(session);
|
||||
}
|
||||
if(now-session.filtersLastUpdated>3600_000L){
|
||||
if(!session.getLocalPreferences().serverSideFiltersSupported && now-session.filtersLastUpdated>3600_000L){
|
||||
updateSessionWordFilters(session);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
|||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
|
||||
import org.joinmastodon.android.ui.views.FilterChipView;
|
||||
|
@ -63,7 +64,9 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
boolean empty=result.isEmpty();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.ACCOUNT);
|
||||
onDataLoaded(result, !empty);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
|
|
@ -33,20 +33,17 @@ import org.joinmastodon.android.events.StatusCreatedEvent;
|
|||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -202,10 +199,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!filters.isEmpty()){
|
||||
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
showNewPostsButton();
|
||||
|
@ -279,19 +273,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
outer:
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(s)){
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
targetList.addAll(buildDisplayItems(s));
|
||||
insertedPosts.add(s);
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, FilterContext.HOME);
|
||||
if(targetList.isEmpty()){
|
||||
// oops. We didn't add new posts, but at least we know there are none.
|
||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||
|
|
|
@ -81,8 +81,8 @@ public class ThreadFragment extends StatusListFragment{
|
|||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
}
|
||||
result.descendants=filterStatuses(result.descendants);
|
||||
result.ancestors=filterStatuses(result.ancestors);
|
||||
filterStatuses(result.descendants);
|
||||
filterStatuses(result.ancestors);
|
||||
if(footerProgress!=null)
|
||||
footerProgress.setVisibility(View.GONE);
|
||||
data.addAll(result.descendants);
|
||||
|
@ -103,17 +103,8 @@ public class ThreadFragment extends StatusListFragment{
|
|||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.THREAD)).collect(Collectors.toList());
|
||||
if(filters.isEmpty())
|
||||
return statuses;
|
||||
return statuses.stream().filter(status->{
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList());
|
||||
private void filterStatuses(List<Status> statuses){
|
||||
AccountSessionManager.get(accountID).filterStatuses(statuses, FilterContext.THREAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,14 +4,13 @@ import android.os.Bundle;
|
|||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
|
@ -27,7 +26,9 @@ public class LocalTimelineFragment extends StatusListFragment{
|
|||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
boolean empty=result.isEmpty();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
|
||||
onDataLoaded(result, !empty);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
|
|
@ -80,7 +80,7 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
|
|||
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
|
||||
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
if(reportStatus!=null){
|
||||
Status hiddenStatus=new Status(reportStatus);
|
||||
Status hiddenStatus=reportStatus.clone();
|
||||
hiddenStatus.spoilerText=getString(R.string.post_hidden);
|
||||
onDataLoaded(Collections.singletonList(hiddenStatus));
|
||||
setTitle(R.string.report_title_post);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.Menu;
|
||||
|
@ -43,7 +42,6 @@ import java.util.stream.Collectors;
|
|||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
|
@ -316,7 +314,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
|||
}
|
||||
|
||||
private boolean isDirty(){
|
||||
return dirty || (filter!=null && !titleEdit.getText().toString().equals(filter.title));
|
||||
return dirty || (filter!=null && !titleEdit.getText().toString().equals(filter.title)) || (filter!=null && (filter.filterAction==FilterAction.WARN)!=cwItem.checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -5,6 +5,7 @@ import org.joinmastodon.android.api.RequiredField;
|
|||
import org.parceler.Parcel;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -22,11 +23,9 @@ public class Filter extends BaseModel{
|
|||
public Instant expiresAt;
|
||||
public FilterAction filterAction;
|
||||
|
||||
@RequiredField
|
||||
public List<FilterKeyword> keywords;
|
||||
public List<FilterKeyword> keywords=new ArrayList<>();
|
||||
|
||||
@RequiredField
|
||||
public List<FilterStatus> statuses;
|
||||
public List<FilterStatus> statuses=new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Parcel
|
||||
public class FilterResult extends BaseModel{
|
||||
@RequiredField
|
||||
public Filter filter;
|
||||
|
||||
public List<String> keywordMatches;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
filter.postprocess();
|
||||
}
|
||||
}
|
|
@ -54,6 +54,10 @@ public class LegacyFilter extends BaseModel{
|
|||
return matches(status.getContentStatus().getStrippedText());
|
||||
}
|
||||
|
||||
public boolean isActive(){
|
||||
return expiresAt==null || expiresAt.isAfter(Instant.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Filter{"+
|
||||
|
|
|
@ -50,6 +50,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||
public Card card;
|
||||
public String language;
|
||||
public String text;
|
||||
public List<FilterResult> filtered;
|
||||
|
||||
public boolean favourited;
|
||||
public boolean reblogged;
|
||||
|
@ -63,39 +64,6 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||
|
||||
public Status(){}
|
||||
|
||||
public Status(Status other){
|
||||
this.id=other.id;
|
||||
this.uri=other.uri;
|
||||
this.createdAt=other.createdAt;
|
||||
this.account=other.account;
|
||||
this.content=other.content;
|
||||
this.visibility=other.visibility;
|
||||
this.sensitive=other.sensitive;
|
||||
this.spoilerText=other.spoilerText;
|
||||
this.mediaAttachments=other.mediaAttachments;
|
||||
this.application=other.application;
|
||||
this.mentions=other.mentions;
|
||||
this.tags=other.tags;
|
||||
this.emojis=other.emojis;
|
||||
this.reblogsCount=other.reblogsCount;
|
||||
this.favouritesCount=other.favouritesCount;
|
||||
this.repliesCount=other.repliesCount;
|
||||
this.editedAt=other.editedAt;
|
||||
this.url=other.url;
|
||||
this.inReplyToId=other.inReplyToId;
|
||||
this.inReplyToAccountId=other.inReplyToAccountId;
|
||||
this.reblog=other.reblog;
|
||||
this.poll=other.poll;
|
||||
this.card=other.card;
|
||||
this.language=other.language;
|
||||
this.text=other.text;
|
||||
this.favourited=other.favourited;
|
||||
this.reblogged=other.reblogged;
|
||||
this.muted=other.muted;
|
||||
this.bookmarked=other.bookmarked;
|
||||
this.pinned=other.pinned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
|
@ -116,6 +84,10 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||
card.postprocess();
|
||||
if(reblog!=null)
|
||||
reblog.postprocess();
|
||||
if(filtered!=null){
|
||||
for(FilterResult fr:filtered)
|
||||
fr.postprocess();
|
||||
}
|
||||
|
||||
spoilerRevealed=!sensitive;
|
||||
}
|
||||
|
@ -139,6 +111,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||
", reblogsCount="+reblogsCount+
|
||||
", favouritesCount="+favouritesCount+
|
||||
", repliesCount="+repliesCount+
|
||||
", editedAt="+editedAt+
|
||||
", url='"+url+'\''+
|
||||
", inReplyToId='"+inReplyToId+'\''+
|
||||
", inReplyToAccountId='"+inReplyToAccountId+'\''+
|
||||
|
@ -147,11 +120,15 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||
", card="+card+
|
||||
", language='"+language+'\''+
|
||||
", text='"+text+'\''+
|
||||
", filtered="+filtered+
|
||||
", favourited="+favourited+
|
||||
", reblogged="+reblogged+
|
||||
", muted="+muted+
|
||||
", bookmarked="+bookmarked+
|
||||
", pinned="+pinned+
|
||||
", spoilerRevealed="+spoilerRevealed+
|
||||
", hasGapAfter="+hasGapAfter+
|
||||
", strippedText='"+strippedText+'\''+
|
||||
'}';
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.displayitems;
|
|||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
@ -12,6 +13,7 @@ import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
|||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
||||
import org.joinmastodon.android.ui.drawables.TiledDrawable;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
|
@ -25,18 +27,25 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
|||
public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>();
|
||||
private final CharSequence parsedTitle;
|
||||
private final CustomEmojiHelper emojiHelper;
|
||||
private final Type type;
|
||||
|
||||
public SpoilerStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
||||
public SpoilerStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String title, Status status, Type type){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
parsedTitle=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedTitle);
|
||||
this.type=type;
|
||||
if(TextUtils.isEmpty(title)){
|
||||
parsedTitle=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedTitle);
|
||||
}else{
|
||||
parsedTitle=title;
|
||||
emojiHelper=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return emojiHelper.getImageCount();
|
||||
return emojiHelper==null ? 0 : emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -46,14 +55,14 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
|||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.SPOILER;
|
||||
return type;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<SpoilerStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView title, action;
|
||||
private final View button;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
public Holder(Context context, ViewGroup parent, Type type){
|
||||
super(context, R.layout.display_item_spoiler, parent);
|
||||
title=findViewById(R.id.spoiler_title);
|
||||
action=findViewById(R.id.spoiler_action);
|
||||
|
@ -62,8 +71,14 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
|||
button.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
button.setClipToOutline(true);
|
||||
LayerDrawable spoilerBg=(LayerDrawable) button.getBackground().mutate();
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(true));
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(false));
|
||||
if(type==Type.SPOILER){
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(true));
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(false));
|
||||
}else if(type==Type.FILTER_SPOILER){
|
||||
Drawable texture=context.getDrawable(R.drawable.filter_banner_stripe_texture);
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new TiledDrawable(texture));
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new TiledDrawable(texture));
|
||||
}
|
||||
button.setBackground(spoilerBg);
|
||||
button.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ package org.joinmastodon.android.ui.displayitems;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
|
@ -14,6 +14,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
|||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
|
@ -72,7 +73,7 @@ public abstract class StatusDisplayItem{
|
|||
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
||||
case SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent);
|
||||
case SPOILER, FILTER_SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent, type);
|
||||
case SECTION_HEADER -> new SectionHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
};
|
||||
|
@ -106,9 +107,24 @@ public abstract class StatusDisplayItem{
|
|||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
}
|
||||
|
||||
boolean filtered=false;
|
||||
if(status.filtered!=null){
|
||||
for(FilterResult filter:status.filtered){
|
||||
if(filter.filter.isActive()){
|
||||
filtered=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(!TextUtils.isEmpty(statusForContent.spoilerText) && AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, statusForContent);
|
||||
if(filtered){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, fragment.getString(R.string.post_matches_filter_x, status.filtered.get(0).filter.title), statusForContent, Type.FILTER_SPOILER);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
statusForContent.spoilerRevealed=false;
|
||||
}else if(!TextUtils.isEmpty(statusForContent.spoilerText) && AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
}else{
|
||||
|
@ -116,7 +132,11 @@ public abstract class StatusDisplayItem{
|
|||
}
|
||||
|
||||
if(!TextUtils.isEmpty(statusForContent.content)){
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent);
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID);
|
||||
if(filtered){
|
||||
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
|
||||
}
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, parsedText, fragment, statusForContent);
|
||||
text.reduceTopPadding=header==null;
|
||||
contentItems.add(text);
|
||||
}else if(header!=null){
|
||||
|
@ -192,7 +212,8 @@ public abstract class StatusDisplayItem{
|
|||
SPOILER,
|
||||
SECTION_HEADER,
|
||||
HEADER_CHECKABLE,
|
||||
NOTIFICATION_HEADER
|
||||
NOTIFICATION_HEADER,
|
||||
FILTER_SPOILER
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class TiledDrawable extends Drawable{
|
||||
private final Drawable drawable;
|
||||
|
||||
public TiledDrawable(Drawable drawable){
|
||||
this.drawable=drawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
Rect bounds=getBounds();
|
||||
canvas.save();
|
||||
canvas.clipRect(bounds);
|
||||
int w=drawable.getIntrinsicWidth();
|
||||
int h=drawable.getIntrinsicHeight();
|
||||
for(int y=bounds.top;y<bounds.bottom;y+=h){
|
||||
for(int x=bounds.left;x<bounds.right;x+=w){
|
||||
drawable.setBounds(x, y, x+w, y+h);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
drawable.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
drawable.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return drawable.getOpacity();
|
||||
}
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.twitter.twittertext.Regex;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
@ -212,4 +217,22 @@ public class HtmlParser{
|
|||
}while(matcher.find()); // Find more URLs
|
||||
return ssb;
|
||||
}
|
||||
|
||||
public static void applyFilterHighlights(Context context, SpannableStringBuilder text, List<FilterResult> filters){
|
||||
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;;
|
||||
for(String word:filter.keywordMatches){
|
||||
Matcher matcher=Pattern.compile("\\b"+Pattern.quote(word)+"\\b", Pattern.CASE_INSENSITIVE).matcher(text);
|
||||
while(matcher.find()){
|
||||
ForegroundColorSpan fg=new ForegroundColorSpan(fgColor);
|
||||
BackgroundColorSpan bg=new BackgroundColorSpan(bgColor);
|
||||
text.setSpan(bg, matcher.start(), matcher.end(), 0);
|
||||
text.setSpan(fg, matcher.start(), matcher.end(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
package org.joinmastodon.android.utils;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StatusFilterPredicate implements Predicate<Status>{
|
||||
private final List<LegacyFilter> filters;
|
||||
|
||||
public StatusFilterPredicate(List<LegacyFilter> filters){
|
||||
this.filters=filters;
|
||||
}
|
||||
|
||||
public StatusFilterPredicate(String accountID, FilterContext context){
|
||||
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Status status){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="42dp"
|
||||
android:height="10dp"
|
||||
android:viewportWidth="42"
|
||||
android:viewportHeight="8">
|
||||
<path
|
||||
android:pathData="M0,0H42V10H-42Z"
|
||||
android:fillColor="#2F0C7A"/>
|
||||
<group android:translateX="-1" android:translateY="-1">
|
||||
<path
|
||||
android:pathData="M6.892,9.333H9.474L9.588,8.687C6.388,7.519 4.277,7.067 1,7.067H0.667V8.533H1C3.269,8.533 4.921,8.759 6.847,9.32L6.892,9.333ZM31.834,8.688L31.951,9.333H34.489L34.535,9.32C36.5,8.748 38.285,8.533 41,8.533H41.333V7.067H41C37.161,7.067 35.016,7.487 31.834,8.688ZM10.219,6.487C14.172,7.965 16.703,8.533 21,8.533C25.273,8.533 27.446,8.012 31.272,6.481C34.946,5.012 36.94,4.533 41,4.533H41.333V3.067H41C36.727,3.067 34.554,3.588 30.728,5.119C27.054,6.588 25.06,7.067 21,7.067C16.905,7.067 14.542,6.537 10.733,5.113C10.711,5.105 10.646,5.081 10.563,5.05C10.385,4.983 10.124,4.885 10.025,4.848C6.577,3.562 4.425,3.067 1,3.067H0.667V4.533H1C4.216,4.533 6.2,4.986 9.513,6.222C9.611,6.259 9.869,6.356 10.046,6.422C10.131,6.454 10.197,6.479 10.219,6.487ZM10.219,2.487C14.172,3.965 16.703,4.533 21,4.533C25.273,4.533 27.446,4.012 31.272,2.481C32.505,1.988 33.545,1.608 34.535,1.32L34.442,0.667H31.891L31.834,0.688C31.479,0.822 31.111,0.966 30.728,1.119C27.054,2.588 25.06,3.067 21,3.067C16.905,3.067 14.542,2.537 10.733,1.113C10.711,1.105 10.646,1.081 10.563,1.05C10.385,0.983 10.124,0.885 10.025,0.848C9.877,0.793 9.731,0.739 9.588,0.687L9.533,0.667H6.94L6.847,1.32C7.661,1.557 8.527,1.855 9.513,2.222C9.611,2.259 9.869,2.356 10.047,2.422C10.131,2.454 10.197,2.479 10.219,2.487Z"
|
||||
android:fillColor="#858AFA"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -631,4 +631,6 @@
|
|||
<string name="app_update_ready">App update ready</string>
|
||||
<string name="app_update_version">Version %s</string>
|
||||
<string name="downloading_update">Downloading (%d%%)</string>
|
||||
<!-- Shown like a content warning, %s is the name of the filter -->
|
||||
<string name="post_matches_filter_x">Matches filter “%s”</string>
|
||||
</resources>
|
Loading…
Reference in New Issue