Merge remote-tracking branch 'megalodon_main/main'
# Conflicts: # mastodon/build.gradle # mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java # mastodon/src/main/java/org/joinmastodon/android/model/Filter.java # mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FileStatusDisplayItem.java # mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java # mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java # mastodon/src/main/res/layout/display_item_file.xml
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
package org.joinmastodon.android.utils;
|
||||||
|
|
||||||
|
import static org.joinmastodon.android.model.Filter.FilterAction.*;
|
||||||
|
import static org.joinmastodon.android.model.Filter.FilterContext.*;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StatusFilterPredicateTest {
|
||||||
|
|
||||||
|
private static final Filter hideMeFilter = new Filter(), warnMeFilter = new Filter();
|
||||||
|
private static final List<Filter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
||||||
|
|
||||||
|
private static final Status
|
||||||
|
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
||||||
|
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now());
|
||||||
|
|
||||||
|
static {
|
||||||
|
hideMeFilter.phrase = "hide me";
|
||||||
|
hideMeFilter.filterAction = HIDE;
|
||||||
|
hideMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||||
|
|
||||||
|
warnMeFilter.phrase = "warning";
|
||||||
|
warnMeFilter.filterAction = WARN;
|
||||||
|
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHide() {
|
||||||
|
assertFalse("should not pass because matching filter applies to given context",
|
||||||
|
new StatusFilterPredicate(allFilters, HOME).test(hideInHomePublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHideRegardlessOfContext() {
|
||||||
|
assertTrue("filters without context should always pass",
|
||||||
|
new StatusFilterPredicate(allFilters, null).test(hideInHomePublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHideInDifferentContext() {
|
||||||
|
assertTrue("should pass because matching filter does not apply to given context",
|
||||||
|
new StatusFilterPredicate(allFilters, THREAD).test(hideInHomePublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHideWithWarningText() {
|
||||||
|
assertTrue("should pass because matching filter is for warnings",
|
||||||
|
new StatusFilterPredicate(allFilters, HOME).test(warnInHomePublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWarn() {
|
||||||
|
assertFalse("should not pass because filter applies to given context",
|
||||||
|
new StatusFilterPredicate(allFilters, HOME, WARN).test(warnInHomePublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWarnRegardlessOfContext() {
|
||||||
|
assertTrue("filters without context should always pass",
|
||||||
|
new StatusFilterPredicate(allFilters, null, WARN).test(warnInHomePublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWarnInDifferentContext() {
|
||||||
|
assertTrue("should pass because filter does not apply to given context",
|
||||||
|
new StatusFilterPredicate(allFilters, THREAD, WARN).test(warnInHomePublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWarnWithHideText() {
|
||||||
|
assertTrue("should pass because matching filter is for hiding",
|
||||||
|
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,10 +74,8 @@ public class CacheController{
|
|||||||
int flags=cursor.getInt(1);
|
int flags=cursor.getInt(1);
|
||||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||||
newMaxID=status.id;
|
newMaxID=status.id;
|
||||||
for(Filter filter:filters){
|
if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
|
||||||
if(filter.matches(status))
|
|
||||||
continue outer;
|
continue outer;
|
||||||
}
|
|
||||||
result.add(status);
|
result.add(status);
|
||||||
}while(cursor.moveToNext());
|
}while(cursor.moveToNext());
|
||||||
String _newMaxID=newMaxID;
|
String _newMaxID=newMaxID;
|
||||||
@@ -92,7 +90,7 @@ public class CacheController{
|
|||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
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));
|
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||||
putHomeTimeline(result, maxID==null);
|
putHomeTimeline(result, maxID==null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,11 +146,9 @@ public class CacheController{
|
|||||||
ntf.postprocess();
|
ntf.postprocess();
|
||||||
newMaxID=ntf.id;
|
newMaxID=ntf.id;
|
||||||
if(ntf.status!=null){
|
if(ntf.status!=null){
|
||||||
for(Filter filter:filters){
|
if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
|
||||||
if(filter.matches(ntf.status))
|
|
||||||
continue outer;
|
continue outer;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
result.add(ntf);
|
result.add(ntf);
|
||||||
}while(cursor.moveToNext());
|
}while(cursor.moveToNext());
|
||||||
String _newMaxID=newMaxID;
|
String _newMaxID=newMaxID;
|
||||||
@@ -170,11 +166,7 @@ public class CacheController{
|
|||||||
public void onSuccess(List<Notification> result){
|
public void onSuccess(List<Notification> result){
|
||||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
|
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
|
||||||
if(ntf.status!=null){
|
if(ntf.status!=null){
|
||||||
for(Filter filter:filters){
|
return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
|
||||||
if(filter.matches(ntf.status)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -51,7 +52,9 @@ public class MastodonAPIController{
|
|||||||
.registerTypeAdapter(Status.class, new Status.StatusDeserializer())
|
.registerTypeAdapter(Status.class, new Status.StatusDeserializer())
|
||||||
.create();
|
.create();
|
||||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||||
|
.readTimeout(5, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
private AccountSession session;
|
private AccountSession session;
|
||||||
private static List<String> badDomains = new ArrayList<>();
|
private static List<String> badDomains = new ArrayList<>();
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ package org.joinmastodon.android.fragments;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||||
@@ -64,7 +60,12 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList());
|
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||||
|
result=result.stream().filter(status -> {
|
||||||
|
// don't hide own posts in own profile
|
||||||
|
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true;
|
||||||
|
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
|
||||||
|
}).collect(Collectors.toList());
|
||||||
onDataLoaded(result, !result.isEmpty());
|
onDataLoaded(result, !result.isEmpty());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -85,7 +86,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||||
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||||
|
if(!asm.isSelf(accountID, ev.status.account) || !asm.isSelf(accountID, user))
|
||||||
return;
|
return;
|
||||||
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||||
@@ -123,4 +125,10 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.ACCOUNT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, DomainDisplay{
|
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, DomainDisplay{
|
||||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||||
protected DisplayItemsAdapter adapter;
|
protected DisplayItemsAdapter adapter;
|
||||||
protected String accountID;
|
protected String accountID;
|
||||||
@@ -271,15 +271,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public @Nullable View getFab() {
|
public @Nullable View getFab() {
|
||||||
if (getParentFragment() instanceof HasFab l) return l.getFab();
|
if (getParentFragment() instanceof HasFab l) return l.getFab();
|
||||||
else return fab;
|
else return fab;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void animateFab(boolean show) {
|
@Override
|
||||||
|
public void showFab() {
|
||||||
View fab = getFab();
|
View fab = getFab();
|
||||||
if (fab == null) return;
|
if (fab == null || fab.getVisibility() == View.VISIBLE) return;
|
||||||
if (show && fab.getVisibility() != View.VISIBLE) {
|
|
||||||
fab.setVisibility(View.VISIBLE);
|
fab.setVisibility(View.VISIBLE);
|
||||||
TranslateAnimation animate = new TranslateAnimation(
|
TranslateAnimation animate = new TranslateAnimation(
|
||||||
0,
|
0,
|
||||||
@@ -288,7 +289,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
0);
|
0);
|
||||||
animate.setDuration(300);
|
animate.setDuration(300);
|
||||||
fab.startAnimation(animate);
|
fab.startAnimation(animate);
|
||||||
} else if (!show && fab.getVisibility() == View.VISIBLE) {
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideFab() {
|
||||||
|
View fab = getFab();
|
||||||
|
if (fab == null || fab.getVisibility() != View.VISIBLE) return;
|
||||||
TranslateAnimation animate = new TranslateAnimation(
|
TranslateAnimation animate = new TranslateAnimation(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -299,7 +305,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
fab.setVisibility(View.INVISIBLE);
|
fab.setVisibility(View.INVISIBLE);
|
||||||
scrollDiff = 0;
|
scrollDiff = 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
@@ -315,10 +320,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
View fab = getFab();
|
View fab = getFab();
|
||||||
if (fab!=null && GlobalUserPreferences.autoHideFab) {
|
if (fab!=null && GlobalUserPreferences.autoHideFab) {
|
||||||
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
||||||
animateFab(false);
|
hideFab();
|
||||||
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
||||||
if (list.getChildAt(0).getTop() == 0 || scrollDiff > THRESHOLD) {
|
if (list.getChildAt(0).getTop() == 0 || scrollDiff > 400) {
|
||||||
animateFab(true);
|
showFab();
|
||||||
scrollDiff = 0;
|
scrollDiff = 0;
|
||||||
} else {
|
} else {
|
||||||
scrollDiff += Math.abs(dy);
|
scrollDiff += Math.abs(dy);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
@@ -35,4 +36,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
|||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.ACCOUNT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
@@ -35,4 +36,9 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
|||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.ACCOUNT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ import android.view.View;
|
|||||||
|
|
||||||
public interface HasFab {
|
public interface HasFab {
|
||||||
View getFab();
|
View getFab();
|
||||||
|
void showFab();
|
||||||
|
void hideFab();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||||
onDataLoaded(result, !result.isEmpty());
|
onDataLoaded(result, !result.isEmpty());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -162,4 +162,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
|||||||
protected void onSetFabBottomInset(int inset){
|
protected void onSetFabBottomInset(int inset){
|
||||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.PUBLIC;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||||
currentTab=savedInstanceState.getInt("selectedTab");
|
currentTab=savedInstanceState.getInt("selectedTab");
|
||||||
|
tabBar.selectTab(currentTab);
|
||||||
Fragment current=fragmentForTab(currentTab);
|
Fragment current=fragmentForTab(currentTab);
|
||||||
|
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
@@ -267,6 +268,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
|
|
||||||
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
||||||
maybeTriggerLoading(newFragment);
|
maybeTriggerLoading(newFragment);
|
||||||
|
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
|
||||||
currentTab=tab;
|
currentTab=tab;
|
||||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -466,10 +466,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
updateSwitcherIcon(i);
|
updateSwitcherIcon(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showFab() {
|
||||||
|
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.showFab();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideFab() {
|
||||||
|
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.hideFab();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSwitcherIcon(int i) {
|
private void updateSwitcherIcon(int i) {
|
||||||
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
||||||
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
||||||
if (fragments[i] instanceof BaseStatusListFragment<?> l) l.animateFab(true);
|
showFab();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.E;
|
|
||||||
import org.joinmastodon.android.R;
|
|
||||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
@@ -156,7 +154,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
result.get(result.size()-1).hasGapAfter=true;
|
result.get(result.size()-1).hasGapAfter=true;
|
||||||
toAdd=result;
|
toAdd=result;
|
||||||
}
|
}
|
||||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
||||||
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
|
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
|
||||||
if(!toAdd.isEmpty()){
|
if(!toAdd.isEmpty()){
|
||||||
prependItems(toAdd, true);
|
prependItems(toAdd, true);
|
||||||
@@ -235,7 +233,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||||
targetList.clear();
|
targetList.clear();
|
||||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
||||||
for(Status s:result){
|
for(Status s:result){
|
||||||
if(idsBelowGap.contains(s.id))
|
if(idsBelowGap.contains(s.id))
|
||||||
break;
|
break;
|
||||||
@@ -288,4 +286,9 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.HOME;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result) {
|
public void onSuccess(List<Status> result) {
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList());
|
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||||
onDataLoaded(result, !result.isEmpty());
|
onDataLoaded(result, !result.isEmpty());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -162,4 +162,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
protected void onSetFabBottomInset(int inset) {
|
protected void onSetFabBottomInset(int inset) {
|
||||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.HOME;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -885,6 +885,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return fab;
|
return fab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showFab() {
|
||||||
|
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.showFab();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideFab() {
|
||||||
|
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.hideFab();
|
||||||
|
}
|
||||||
|
|
||||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||||
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
||||||
if(scrollY>avatarBorder.getTop()-topBarsH){
|
if(scrollY>avatarBorder.getTop()-topBarsH){
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
||||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, Filter.FilterContext.HOME);
|
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, Filter.FilterContext.HOME);
|
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, null);
|
||||||
int idx=data.indexOf(s);
|
int idx=data.indexOf(s);
|
||||||
if(idx>=0){
|
if(idx>=0){
|
||||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||||
@@ -157,4 +157,9 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
|||||||
public boolean isItemEnabled(String id){
|
public boolean isItemEnabled(String id){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
|||||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
|
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
|
||||||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
|
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
|
||||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME);
|
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract Filter.FilterContext getFilterContext();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void addAccountToKnown(Status s){
|
protected void addAccountToKnown(Status s){
|
||||||
if(!knownAccounts.containsKey(s.account.id))
|
if(!knownAccounts.containsKey(s.account.id))
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Status> filterStatuses(List<Status> statuses){
|
private List<Status> filterStatuses(List<Status> statuses){
|
||||||
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
|
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
|
||||||
return statuses.stream()
|
return statuses.stream()
|
||||||
.filter(statusFilterPredicate)
|
.filter(statusFilterPredicate)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -189,4 +189,10 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
|||||||
public boolean wantsLightNavigationBar(){
|
public boolean wantsLightNavigationBar(){
|
||||||
return !UiUtils.isDarkTheme();
|
return !UiUtils.isDarkTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.THREAD;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||||
onDataLoaded(result, !result.isEmpty());
|
onDataLoaded(result, !result.isEmpty());
|
||||||
}
|
}
|
||||||
}).exec(accountID);
|
}).exec(accountID);
|
||||||
@@ -47,4 +47,10 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
|
|||||||
public boolean isOnTop() {
|
public boolean isOnTop() {
|
||||||
return isRecyclerViewOnTop(list);
|
return isRecyclerViewOnTop(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.PUBLIC;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class FederatedTimelineFragment extends StatusListFragment {
|
|||||||
if(!result.isEmpty())
|
if(!result.isEmpty())
|
||||||
maxID=result.get(result.size()-1).id;
|
maxID=result.get(result.size()-1).id;
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||||
onDataLoaded(result, !result.isEmpty());
|
onDataLoaded(result, !result.isEmpty());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -52,4 +52,9 @@ public class FederatedTimelineFragment extends StatusListFragment {
|
|||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
// bannerHelper.maybeAddBanner(contentWrap);
|
// bannerHelper.maybeAddBanner(contentWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.PUBLIC;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class LocalTimelineFragment extends StatusListFragment {
|
|||||||
if(!result.isEmpty())
|
if(!result.isEmpty())
|
||||||
maxID=result.get(result.size()-1).id;
|
maxID=result.get(result.size()-1).id;
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||||
onDataLoaded(result, !result.isEmpty());
|
onDataLoaded(result, !result.isEmpty());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -50,4 +50,9 @@ public class LocalTimelineFragment extends StatusListFragment {
|
|||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
bannerHelper.maybeAddBanner(contentWrap);
|
bannerHelper.maybeAddBanner(contentWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.PUBLIC;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
|
|||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
|
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
|
||||||
list.setItemAnimator(new BetterItemAnimator());
|
list.setItemAnimator(new BetterItemAnimator());
|
||||||
|
((UsableRecyclerView) list).setSelector(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -262,4 +262,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
|||||||
protected boolean wantsOverlaySystemNavigation(){
|
protected boolean wantsOverlaySystemNavigation(){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,24 @@ package org.joinmastodon.android.model;
|
|||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public class Filter extends BaseModel{
|
public class Filter extends BaseModel{
|
||||||
@RequiredField
|
|
||||||
public String id;
|
public String id;
|
||||||
@RequiredField
|
|
||||||
public String title;
|
|
||||||
@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;
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package org.joinmastodon.android.model;
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public class FilterResult extends BaseModel {
|
public class FilterResult extends BaseModel {
|
||||||
public Filter filter;
|
public Filter filter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess() throws ObjectValidationException {
|
||||||
|
super.postprocess();
|
||||||
|
if (filter != null) filter.postprocess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
public long favouritesCount;
|
public long favouritesCount;
|
||||||
public long repliesCount;
|
public long repliesCount;
|
||||||
public Instant editedAt;
|
public Instant editedAt;
|
||||||
|
// might not be provided (by older mastodon servers),
|
||||||
|
// so megalodon will use the locally cached filters if filtered == null
|
||||||
public List<FilterResult> filtered;
|
public List<FilterResult> filtered;
|
||||||
|
|
||||||
public String url;
|
public String url;
|
||||||
@@ -107,6 +109,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
card.postprocess();
|
card.postprocess();
|
||||||
if(reblog!=null)
|
if(reblog!=null)
|
||||||
reblog.postprocess();
|
reblog.postprocess();
|
||||||
|
if(filtered!=null)
|
||||||
|
for(FilterResult fr : filtered)
|
||||||
|
fr.postprocess();
|
||||||
|
|
||||||
spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive;
|
spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive;
|
||||||
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;
|
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;
|
||||||
@@ -188,7 +193,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
s.mentions = List.of();
|
s.mentions = List.of();
|
||||||
s.tags = List.of();
|
s.tags = List.of();
|
||||||
s.emojis = List.of();
|
s.emojis = List.of();
|
||||||
s.filtered = List.of();
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ public class FileStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<FileStatusDisplayItem>{
|
public static class Holder extends StatusDisplayItem.Holder<FileStatusDisplayItem>{
|
||||||
private final TextView title, domain;
|
private final TextView title, domain;
|
||||||
private boolean didClear;
|
|
||||||
|
|
||||||
public Holder(Context context, ViewGroup parent){
|
public Holder(Context context, ViewGroup parent){
|
||||||
super(context, R.layout.display_item_file, parent);
|
super(context, R.layout.display_item_file, parent);
|
||||||
@@ -54,6 +53,21 @@ public class FileStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
private void onClick(View v){
|
private void onClick(View v){
|
||||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.attachment.remoteUrl == null ? item.attachment.url : item.attachment.remoteUrl);
|
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.attachment.remoteUrl == null ? item.attachment.url : item.attachment.remoteUrl);
|
||||||
|
public void onBind(FileStatusDisplayItem item) {
|
||||||
|
Uri url = Uri.parse(getUrl());
|
||||||
|
title.setText(item.attachment.description != null
|
||||||
|
? item.attachment.description
|
||||||
|
: url.getLastPathSegment());
|
||||||
|
title.setEllipsize(item.attachment.description != null ? TextUtils.TruncateAt.END : TextUtils.TruncateAt.MIDDLE);
|
||||||
|
domain.setText(url.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClick(View v) {
|
||||||
|
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUrl() {
|
||||||
|
return item.attachment.remoteUrl == null ? item.attachment.url : item.attachment.remoteUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||||
@@ -83,18 +82,10 @@ public abstract class StatusDisplayItem{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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, 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, Filter.FilterContext filterContext){
|
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);
|
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){
|
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<>();
|
||||||
@@ -104,14 +95,6 @@ public abstract class StatusDisplayItem{
|
|||||||
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 != null && !statusForContent.filterRevealed){
|
|
||||||
statusForContent.filterRevealed = filterPredicate.testWithWarning(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
||||||
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
||||||
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
||||||
@@ -186,7 +169,10 @@ public abstract class StatusDisplayItem{
|
|||||||
replyLine.needBottomPadding=true;
|
replyLine.needBottomPadding=true;
|
||||||
else
|
else
|
||||||
header.needBottomPadding=true;
|
header.needBottomPadding=true;
|
||||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage() && !att.type.equals(Attachment.Type.UNKNOWN)).collect(Collectors.toList());
|
|
||||||
|
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream()
|
||||||
|
.filter(att->att.type.isImage() && !att.type.equals(Attachment.Type.UNKNOWN))
|
||||||
|
.collect(Collectors.toList());
|
||||||
if(!imageAttachments.isEmpty()){
|
if(!imageAttachments.isEmpty()){
|
||||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||||
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
||||||
@@ -197,22 +183,19 @@ public abstract class StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Attachment> fileAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.equals(Attachment.Type.UNKNOWN)).collect(Collectors.toList());
|
statusForContent.mediaAttachments.stream()
|
||||||
fileAttachments.forEach(attachment -> {
|
.filter(att->att.type.equals(Attachment.Type.UNKNOWN))
|
||||||
items.add(new FileStatusDisplayItem(parentID, fragment, attachment, statusForContent));
|
.map(att -> new FileStatusDisplayItem(parentID, fragment, att))
|
||||||
});
|
.forEach(items::add);
|
||||||
|
|
||||||
if(statusForContent.poll!=null){
|
if(statusForContent.poll!=null){
|
||||||
buildPollItems(parentID, fragment, statusForContent.poll, items, statusForContent);
|
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
||||||
}
|
}
|
||||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && TextUtils.isEmpty(statusForContent.spoilerText)){
|
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||||
items.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
items.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||||
}
|
}
|
||||||
if(addFooter){
|
if(addFooter){
|
||||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||||
if(status.hasGapAfter && !(fragment instanceof ThreadFragment)){
|
|
||||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
int i=1;
|
int i=1;
|
||||||
for(StatusDisplayItem item:items){
|
for(StatusDisplayItem item:items){
|
||||||
@@ -220,20 +203,30 @@ public abstract class StatusDisplayItem{
|
|||||||
item.index=i++;
|
item.index=i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Filter applyingFilter = null;
|
||||||
if (!statusForContent.filterRevealed) {
|
if (!statusForContent.filterRevealed) {
|
||||||
return new ArrayList<>(List.of(
|
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, Filter.FilterAction.WARN);
|
||||||
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)
|
statusForContent.filterRevealed = predicate.test(status);
|
||||||
));
|
applyingFilter = predicate.getApplyingFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
ArrayList<StatusDisplayItem> result = statusForContent.filterRevealed ? items :
|
||||||
|
new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items, applyingFilter)));
|
||||||
|
|
||||||
|
if (addFooter && status.hasGapAfter && !(fragment instanceof ThreadFragment)) {
|
||||||
|
StatusDisplayItem gap = new GapStatusDisplayItem(parentID, fragment);
|
||||||
|
gap.index = i++;
|
||||||
|
result.add(gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items, Status status){
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){
|
||||||
for(Poll.Option opt:poll.options){
|
for(Poll.Option opt:poll.options){
|
||||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, opt, fragment, status));
|
items.add(new PollOptionStatusDisplayItem(parentID, poll, opt, fragment));
|
||||||
}
|
}
|
||||||
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll, status));
|
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll));
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Type{
|
public enum Type{
|
||||||
@@ -249,9 +242,9 @@ public abstract class StatusDisplayItem{
|
|||||||
ACCOUNT,
|
ACCOUNT,
|
||||||
HASHTAG,
|
HASHTAG,
|
||||||
GAP,
|
GAP,
|
||||||
WARNING,
|
|
||||||
EXTENDED_FOOTER,
|
EXTENDED_FOOTER,
|
||||||
MEDIA_GRID,
|
MEDIA_GRID,
|
||||||
|
WARNING,
|
||||||
FILE
|
FILE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
|
import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
|
||||||
|
|
||||||
@@ -20,12 +21,14 @@ import java.util.ArrayList;
|
|||||||
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||||
public boolean loading;
|
public boolean loading;
|
||||||
public final Status status;
|
public final Status status;
|
||||||
public ArrayList<StatusDisplayItem> filteredItems;
|
public List<StatusDisplayItem> filteredItems;
|
||||||
|
public Filter applyingFilter;
|
||||||
|
|
||||||
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, ArrayList<StatusDisplayItem> items){
|
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems, Filter applyingFilter){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
this.status=status;
|
this.status=status;
|
||||||
this.filteredItems = items;
|
this.filteredItems = filteredItems;
|
||||||
|
this.applyingFilter = applyingFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -49,9 +52,9 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(WarningFilteredStatusDisplayItem item){
|
public void onBind(WarningFilteredStatusDisplayItem item) {
|
||||||
filteredItems = item.filteredItems;
|
filteredItems = item.filteredItems;
|
||||||
text.setText(item.parentFragment.getString(R.string.mo_filtered, item.status.filtered.get(item.status.filtered.size() -1).filter.title));
|
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.applyingFilter.title));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,54 +6,80 @@ import org.joinmastodon.android.model.Status;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class StatusFilterPredicate implements Predicate<Status>{
|
public class StatusFilterPredicate implements Predicate<Status>{
|
||||||
private final List<Filter> filters;
|
private final List<Filter> filters;
|
||||||
|
private final Filter.FilterContext context;
|
||||||
|
private final Filter.FilterAction action;
|
||||||
|
private Filter applyingFilter;
|
||||||
|
|
||||||
public StatusFilterPredicate(List<Filter> filters){
|
/**
|
||||||
this.filters=filters;
|
* @param context null makes the predicate pass automatically
|
||||||
|
* @param action defines what the predicate should check:
|
||||||
|
* status should not be hidden or should not display with warning
|
||||||
|
*/
|
||||||
|
public StatusFilterPredicate(List<Filter> filters, Filter.FilterContext context, Filter.FilterAction action){
|
||||||
|
this.filters = filters;
|
||||||
|
this.context = context;
|
||||||
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StatusFilterPredicate(String accountID, Filter.FilterContext context){
|
public StatusFilterPredicate(List<Filter> filters, Filter.FilterContext context){
|
||||||
|
this(filters, context, Filter.FilterAction.HIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context null makes the predicate pass automatically
|
||||||
|
* @param action defines what the predicate should check:
|
||||||
|
* status should not be hidden or should not display with warning
|
||||||
|
*/
|
||||||
|
public StatusFilterPredicate(String accountID, Filter.FilterContext context, Filter.FilterAction action){
|
||||||
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
|
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
|
||||||
|
this.context = context;
|
||||||
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context null makes the predicate pass automatically
|
||||||
|
*/
|
||||||
|
public StatusFilterPredicate(String accountID, Filter.FilterContext context){
|
||||||
|
this(accountID, context, Filter.FilterAction.HIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return whether the status should be displayed without being hidden/warned about.
|
||||||
|
* will always return true if the context is null.
|
||||||
|
* true = display this status,
|
||||||
|
* false = filter this status
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean test(Status status){
|
public boolean test(Status status){
|
||||||
if(status.filtered!=null){
|
if (context == null) return true;
|
||||||
if (status.filtered.isEmpty()){
|
|
||||||
return true;
|
Stream<Filter> matchingFilters = status.filtered != null
|
||||||
}
|
// use server-provided per-status info (status.filtered) if available
|
||||||
boolean matches=status.filtered.stream()
|
? status.filtered.stream().map(f -> f.filter)
|
||||||
.map(filterResult->filterResult.filter)
|
// or fall back to cached filters
|
||||||
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
|
: filters.stream().filter(filter -> filter.matches(status));
|
||||||
.anyMatch(filter->filter.filterAction==Filter.FilterAction.HIDE);
|
|
||||||
return !matches;
|
Optional<Filter> applyingFilter = matchingFilters
|
||||||
}
|
// discard expired filters
|
||||||
for(Filter filter:filters){
|
.filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now()))
|
||||||
if(filter.matches(status))
|
// only apply filters for given context
|
||||||
return false;
|
.filter(filter -> filter.context.contains(context))
|
||||||
}
|
// treating filterAction = null (from filters list) as FilterAction.HIDE
|
||||||
return true;
|
.filter(filter -> filter.filterAction == null ? action == Filter.FilterAction.HIDE : filter.filterAction == action)
|
||||||
|
.findAny();
|
||||||
|
|
||||||
|
this.applyingFilter = applyingFilter.orElse(null);
|
||||||
|
return applyingFilter.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean testWithWarning(Status status) {
|
public Filter getApplyingFilter() {
|
||||||
if(status.filtered!=null){
|
return applyingFilter;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
mastodon/src/main/res/drawable/bg_search_button.xml
Normal file
6
mastodon/src/main/res/drawable/bg_search_button.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="?android:attr/colorControlHighlight">
|
||||||
|
<item android:drawable="@drawable/bg_search_field" />
|
||||||
|
</ripple>
|
||||||
@@ -55,5 +55,4 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
|
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/bg_search_field">
|
android:background="@drawable/bg_search_button">
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
Reference in New Issue
Block a user