Filtered posts in timelines (AND-8)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user