Display filtered posts with a warning (#406)
* copy changes from @LucasGGamerM * simplify building filter item * fix adapter ranges * change filter item styling closes sk22#209 Co-authored-by: LucasGGamerM <71328265+LucasGGamerM@users.noreply.github.com>
This commit is contained in:
parent
33d856562d
commit
618840c76a
|
@ -40,6 +40,7 @@ import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
@ -504,6 +505,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||||
|
|
||||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||||
|
|
||||||
|
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||||
|
int startPos = warning.getAbsoluteAdapterPosition();
|
||||||
|
displayItems.remove(startPos);
|
||||||
|
displayItems.addAll(startPos, warning.filteredItems);
|
||||||
|
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
||||||
|
if (startPos == 0) scrollToTop();
|
||||||
|
}
|
||||||
|
|
||||||
public String getAccountID(){
|
public String getAccountID(){
|
||||||
return accountID;
|
return accountID;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
@ -91,7 +92,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
};
|
};
|
||||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, extraText, n, null) : null;
|
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, extraText, n, null) : null;
|
||||||
if(n.status!=null){
|
if(n.status!=null){
|
||||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n);
|
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
|
||||||
if(titleItem!=null){
|
if(titleItem!=null){
|
||||||
for(StatusDisplayItem item:items){
|
for(StatusDisplayItem item:items){
|
||||||
if(item instanceof ImageStatusDisplayItem imgItem){
|
if(item instanceof ImageStatusDisplayItem imgItem){
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
|
@ -30,7 +31,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||||
protected EventListener eventListener=new EventListener();
|
protected EventListener eventListener=new EventListener();
|
||||||
|
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null);
|
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null, Filter.FilterContext.HOME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -56,6 +57,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||||
Status status=getContentStatusByID(id);
|
Status status=getContentStatusByID(id);
|
||||||
if(status==null)
|
if(status==null)
|
||||||
return;
|
return;
|
||||||
|
status.filterRevealed = true;
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putParcelable("status", Parcels.wrap(status));
|
args.putParcelable("status", Parcels.wrap(status));
|
||||||
|
|
|
@ -19,6 +19,7 @@ public class Filter extends BaseModel{
|
||||||
public String id;
|
public String id;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String phrase;
|
public String phrase;
|
||||||
|
public String title;
|
||||||
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);
|
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);
|
||||||
public Instant expiresAt;
|
public Instant expiresAt;
|
||||||
public boolean irreversible;
|
public boolean irreversible;
|
||||||
|
@ -50,6 +51,7 @@ public class Filter extends BaseModel{
|
||||||
else
|
else
|
||||||
pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
|
pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
|
||||||
}
|
}
|
||||||
|
if (title == null) title = phrase;
|
||||||
return pattern.matcher(text).find();
|
return pattern.matcher(text).find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +63,7 @@ public class Filter extends BaseModel{
|
||||||
public String toString(){
|
public String toString(){
|
||||||
return "Filter{"+
|
return "Filter{"+
|
||||||
"id='"+id+'\''+
|
"id='"+id+'\''+
|
||||||
|
", title='"+title+'\''+
|
||||||
", phrase='"+phrase+'\''+
|
", phrase='"+phrase+'\''+
|
||||||
", context="+context+
|
", context="+context+
|
||||||
", expiresAt="+expiresAt+
|
", expiresAt="+expiresAt+
|
||||||
|
|
|
@ -58,6 +58,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||||
public boolean bookmarked;
|
public boolean bookmarked;
|
||||||
public boolean pinned;
|
public boolean pinned;
|
||||||
|
|
||||||
|
public transient boolean filterRevealed;
|
||||||
public transient boolean spoilerRevealed;
|
public transient boolean spoilerRevealed;
|
||||||
public transient boolean hasGapAfter;
|
public transient boolean hasGapAfter;
|
||||||
private transient String strippedText;
|
private transient String strippedText;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.Poll;
|
import org.joinmastodon.android.model.Poll;
|
||||||
|
@ -26,6 +27,7 @@ import org.joinmastodon.android.model.ScheduledStatus;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
|
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -79,21 +81,39 @@ public abstract class StatusDisplayItem{
|
||||||
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
||||||
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
||||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||||
|
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
|
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
|
||||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false);
|
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, Filter.FilterContext.HOME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
|
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){
|
||||||
|
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
|
||||||
|
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, Filter.FilterContext.HOME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
|
||||||
String parentID=parentObject.getID();
|
String parentID=parentObject.getID();
|
||||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||||
|
|
||||||
Status statusForContent=status.getContentStatus();
|
Status statusForContent=status.getContentStatus();
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
||||||
|
|
||||||
|
List<Filter> filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream()
|
||||||
|
.filter(f -> f.context.contains(filterContext)).collect(Collectors.toList());
|
||||||
|
StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters);
|
||||||
|
|
||||||
|
if(!statusForContent.filterRevealed){
|
||||||
|
statusForContent.filterRevealed = filterPredicate.testWithWarning(status);
|
||||||
|
}
|
||||||
|
|
||||||
if(status.reblog!=null){
|
if(status.reblog!=null){
|
||||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
|
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
|
||||||
|
@ -170,6 +190,13 @@ public abstract class StatusDisplayItem{
|
||||||
item.inset=inset;
|
item.inset=inset;
|
||||||
item.index=i++;
|
item.index=i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!statusForContent.filterRevealed) {
|
||||||
|
return new ArrayList<>(List.of(
|
||||||
|
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,6 +223,7 @@ public abstract class StatusDisplayItem{
|
||||||
ACCOUNT,
|
ACCOUNT,
|
||||||
HASHTAG,
|
HASHTAG,
|
||||||
GAP,
|
GAP,
|
||||||
|
WARNING,
|
||||||
EXTENDED_FOOTER
|
EXTENDED_FOOTER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +233,7 @@ public abstract class StatusDisplayItem{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Holder(Context context, int layout, ViewGroup parent){
|
public Holder(Context context, int layout, ViewGroup parent){
|
||||||
super(context, layout, parent);
|
super(context, layout, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getItemID(){
|
public String getItemID(){
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.joinmastodon.android.ui.displayitems;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||||
|
public boolean loading;
|
||||||
|
public final Status status;
|
||||||
|
public List<StatusDisplayItem> filteredItems;
|
||||||
|
|
||||||
|
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems){
|
||||||
|
super(parentID, parentFragment);
|
||||||
|
this.status=status;
|
||||||
|
this.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getType(){
|
||||||
|
return Type.WARNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Holder extends StatusDisplayItem.Holder<WarningFilteredStatusDisplayItem>{
|
||||||
|
public final View warningWrap;
|
||||||
|
public final TextView text;
|
||||||
|
public List<StatusDisplayItem> filteredItems;
|
||||||
|
|
||||||
|
public Holder(Context context, ViewGroup parent) {
|
||||||
|
super(context, R.layout.display_item_filter_warning, parent);
|
||||||
|
warningWrap=findViewById(R.id.warning_wrap);
|
||||||
|
text=findViewById(R.id.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBind(WarningFilteredStatusDisplayItem item) {
|
||||||
|
filteredItems = item.filteredItems;
|
||||||
|
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.status.filtered.get(item.status.filtered.size() -1).filter.title));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
item.parentFragment.onWarningClick(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,4 +38,22 @@ public class StatusFilterPredicate implements Predicate<Status>{
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean testWithWarning(Status status) {
|
||||||
|
if(status.filtered!=null){
|
||||||
|
if (status.filtered.isEmpty()){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
boolean matches=status.filtered.stream()
|
||||||
|
.map(filterResult->filterResult.filter)
|
||||||
|
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
|
||||||
|
.anyMatch(filter->filter.filterAction==Filter.FilterAction.WARN);
|
||||||
|
return !matches;
|
||||||
|
}
|
||||||
|
for(Filter filter:filters){
|
||||||
|
if(filter.matches(status))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/warning_wrap"
|
||||||
|
android:background="@drawable/bg_timeline_gap">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start|center_vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textAppearance="@style/m3_title_medium"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:text="@string/sk_filtered"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reveal_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/tap_to_reveal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textColor="?android:textColorSecondary" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -252,4 +252,5 @@
|
||||||
<string name="sk_settings_server_version">Server version: %s</string>
|
<string name="sk_settings_server_version">Server version: %s</string>
|
||||||
<string name="sk_notify_poll_results">Poll results</string>
|
<string name="sk_notify_poll_results">Poll results</string>
|
||||||
<string name="sk_settings_prefix_reply_cw_with_re">Prefix reply CW with “re:”</string>
|
<string name="sk_settings_prefix_reply_cw_with_re">Prefix reply CW with “re:”</string>
|
||||||
|
<string name="sk_filtered">Filtered: %s</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue