Reporting

This commit is contained in:
Grishka
2022-03-07 14:01:40 +03:00
parent abae198e89
commit 86892e4103
36 changed files with 1411 additions and 20 deletions

View File

@@ -22,12 +22,17 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {}
case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true");
addQueryParameter("exclude_reblogs", "true");
}
}
}
public enum Filter{
DEFAULT,
INCLUDE_REPLIES,
MEDIA
MEDIA,
NO_REBLOGS
}
}

View File

@@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.reports;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ReportReason;
import java.util.Collections;
import java.util.List;
public class SendReport extends MastodonAPIRequest<Object>{
public SendReport(String accountID, ReportReason reason, List<String> statusIDs, List<String> ruleIDs, String comment, boolean forward){
super(HttpMethod.POST, "/reports", Object.class);
Body b=new Body();
b.accountId=accountID;
b.statusIds=statusIDs;
b.comment=comment;
b.forward=forward;
b.category=reason;
b.ruleIds=ruleIDs;
setRequestBody(b);
}
private static class Body{
public String accountId;
public List<String> statusIds;
public String comment;
public boolean forward;
public ReportReason category;
public List<String> ruleIds;
}
}

View File

@@ -4,6 +4,7 @@ import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
public class AccountSession{
@@ -13,16 +14,19 @@ public class AccountSession{
public int tootCharLimit;
public Application app;
public long infoLastUpdated;
public long instanceLastUpdated;
public Instance instance;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit){
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit, Instance instance){
this.token=token;
this.self=self;
this.domain=domain;
this.app=app;
this.tootCharLimit=tootCharLimit;
infoLastUpdated=System.currentTimeMillis();
this.instance=instance;
instanceLastUpdated=infoLastUpdated=System.currentTimeMillis();
}
AccountSession(){}

View File

@@ -13,6 +13,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
@@ -84,7 +85,7 @@ public class AccountSessionManager{
}
public void addAccount(Instance instance, Token token, Account self, Application app){
AccountSession session=new AccountSession(token, self, app, instance.uri, instance.maxTootChars);
AccountSession session=new AccountSession(token, self, app, instance.uri, instance.maxTootChars, instance);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
@@ -212,7 +213,7 @@ public class AccountSessionManager{
HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase());
if(now-session.infoLastUpdated>24L*3600_000L){
if(now-session.infoLastUpdated>24L*3600_000L || now-session.instanceLastUpdated>24L*360_000L*3L){
updateSessionLocalInfo(session);
}
}
@@ -247,6 +248,21 @@ public class AccountSessionManager{
}
})
.exec(session.getID());
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
session.instance=result;
session.instanceLastUpdated=System.currentTimeMillis();
writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
}
})
.exec(session.getID());
}
private void updateCustomEmojis(String domain){

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.events;
public class FinishReportFragmentsEvent{
public final String reportAccountID;
public FinishReportFragmentsEvent(String reportAccountID){
this.reportAccountID=reportAccountID;
}
}

View File

@@ -0,0 +1,192 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseReportChoiceFragment extends ToolbarFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
private View buttonBar;
protected ArrayList<Item> items=new ArrayList<>();
protected boolean isMultipleChoice;
protected ArrayList<String> selectedIDs=new ArrayList<>();
protected String accountID;
protected Account reportAccount;
protected Status reportStatus;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
E.register(this);
}
@Override
public void onDestroy(){
E.unregister(this);
super.onDestroy();
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
setTitle(getString(R.string.report_title, reportAccount.acct));
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_report_choice, container, false);
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
populateItems();
Item header=getHeaderItem();
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
title.setText(header.title);
subtitle.setText(header.subtitle);
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
return view;
}
protected abstract Item getHeaderItem();
protected abstract void populateItems();
protected abstract void onButtonClick();
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
protected static class Item{
public String title, subtitle, id;
public Item(String title, String subtitle, String id){
this.title=title;
this.subtitle=subtitle;
this.id=id;
}
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ItemViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
holder.bind(items.get(position));
}
@Override
public int getItemCount(){
return items.size();
}
}
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView title, subtitle;
private final ImageView checkbox;
public ItemViewHolder(){
super(getActivity(), R.layout.item_report_choice, list);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
checkbox=findViewById(R.id.checkbox);
}
@Override
public void onBind(Item item){
title.setText(item.title);
if(TextUtils.isEmpty(item.subtitle)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.subtitle);
}
checkbox.setSelected(selectedIDs.contains(item.id));
}
@Override
public void onClick(){
if(isMultipleChoice){
if(selectedIDs.contains(item.id))
selectedIDs.remove(item.id);
else
selectedIDs.add(item.id);
rebind();
}else{
if(!selectedIDs.contains(item.id)){
if(!selectedIDs.isEmpty()){
String prev=selectedIDs.remove(0);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof ItemViewHolder && ((ItemViewHolder) holder).getItem().id.equals(prev)){
((ItemViewHolder) holder).rebind();
break;
}
}
}
selectedIDs.add(item.id);
rebind();
}
}
btn.setEnabled(!selectedIDs.isEmpty());
}
}
}

View File

@@ -67,6 +67,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected PhotoViewer currentPhotoViewer;
protected HashMap<String, Account> knownAccounts=new HashMap<>();
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect();
public BaseStatusListFragment(){
super(20);
@@ -259,7 +260,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
});
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Rect tmpRect=new Rect();
private Paint paint=new Paint();
{
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
@@ -276,11 +276,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder && siblingHolder instanceof StatusDisplayItem.Holder
&& !((StatusDisplayItem.Holder<?>) holder).getItemID().equals(((StatusDisplayItem.Holder<?>) siblingHolder).getItemID())){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
tmpRect.offset(0, Math.round(child.getTranslationY()));
float y=tmpRect.bottom-V.dp(.5f);
paint.setAlpha(Math.round(255*child.getAlpha()));
c.drawLine(0, y, parent.getWidth(), y, paint);
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, paint);
}
}
}
@@ -398,6 +394,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return 0;
}
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
tmpRect.offset(0, Math.round(child.getTranslationY()));
float y=tmpRect.bottom-V.dp(.5f);
paint.setAlpha(Math.round(255*child.getAlpha()));
c.drawLine(0, y, parent.getWidth(), y, paint);
}
public abstract void onItemClick(String id);
protected void updatePoll(String itemID, Poll poll){
@@ -494,7 +498,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
status.spoilerRevealed=true;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null)
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null)
header.rebind();
@@ -509,7 +513,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
}
}
holder.rebind();
@@ -609,7 +613,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@NonNull
@Override
public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType], getActivity(), parent);
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent);
}
@Override
@@ -625,7 +629,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public int getItemViewType(int position){
return displayItems.get(position).getType().ordinal();
return displayItems.get(position).getType().ordinal() | 0x80000000;
}
@Override

View File

@@ -476,6 +476,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
confirmToggleMuted();
}else if(id==R.id.block){
confirmToggleBlocked();
}else if(id==R.id.report){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
}
return true;
}

View File

@@ -0,0 +1,263 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseIntArray;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class ReportAddPostsChoiceFragment extends StatusListFragment{
private Button btn;
private View buttonBar;
private ArrayList<String> selectedIDs=new ArrayList<>();
private String accountID;
private Account reportAccount;
private Status reportStatus;
private SparseIntArray knownDisplayItemHeights=new SparseIntArray();
private HashSet<String> postsWithKnownNonHeaderHeights=new HashSet<>();
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setListLayoutId(R.layout.fragment_content_report_posts);
setLayout(R.layout.fragment_report_posts);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
if(reportStatus!=null)
selectedIDs.add(reportStatus.id);
setTitle(getString(R.string.report_title, reportAccount.acct));
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.NO_REBLOGS)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onItemClick(String id){
if(selectedIDs.contains(id))
selectedIDs.remove(id);
else
selectedIDs.add(id);
list.invalidate();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Drawable uncheckedIcon=getResources().getDrawable(R.drawable.ic_fluent_radio_button_24_regular, getActivity().getTheme()).mutate();
private Drawable checkedIcon=getResources().getDrawable(R.drawable.ic_fluent_checkmark_circle_24_filled, getActivity().getTheme()).mutate();
{
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary);
checkedIcon.setTint(color);
uncheckedIcon.setTint(color);
checkedIcon.setBounds(0, 0, checkedIcon.getIntrinsicWidth(), checkedIcon.getIntrinsicHeight());
uncheckedIcon.setBounds(0, 0, uncheckedIcon.getIntrinsicWidth(), uncheckedIcon.getIntrinsicHeight());
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder.getAbsoluteAdapterPosition()==0)
return;
outRect.left=V.dp(40);
if(holder instanceof ImageStatusDisplayItem.Holder){
ImageStatusDisplayItem.Holder<ImageStatusDisplayItem> imgHolder=(ImageStatusDisplayItem.Holder<ImageStatusDisplayItem>) holder;
String siblingID;
if(holder.getAbsoluteAdapterPosition()<parent.getAdapter().getItemCount()-1){
siblingID=displayItems.get(holder.getAbsoluteAdapterPosition()-getMainAdapterOffset()+1).parentID;
}else{
siblingID=null;
}
outRect.left+=V.dp(16);
outRect.right=V.dp(16);
if(!imgHolder.getItemID().equals(siblingID) || imgHolder.getItem().thisTile.startRow+imgHolder.getItem().thisTile.rowSpan==imgHolder.getItem().tiledLayout.rowSizes.length)
outRect.bottom=V.dp(16);
}else if(holder instanceof AudioStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
}else if(holder instanceof LinkCardStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
outRect.left+=V.dp(16);
outRect.right=V.dp(16);
}
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
// 1st pass: update item heights
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
String id=((StatusDisplayItem.Holder<?>) holder).getItemID();
int height=tmpRect.height();
if(holder instanceof ImageStatusDisplayItem.Holder){
ImageStatusDisplayItem.Holder<ImageStatusDisplayItem> imgHolder=(ImageStatusDisplayItem.Holder<ImageStatusDisplayItem>) holder;
if(imgHolder.getItem().thisTile.startCol+imgHolder.getItem().thisTile.colSpan<imgHolder.getItem().tiledLayout.columnSizes.length)
height=0;
}
if(!(holder instanceof HeaderStatusDisplayItem.Holder) && !(holder instanceof ReblogOrReplyLineStatusDisplayItem.Holder))
postsWithKnownNonHeaderHeights.add(id);
knownDisplayItemHeights.put(holder.getAbsoluteAdapterPosition(), height);
}
}
// 2nd pass: draw checkboxes
String lastPostID=null;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
String postID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(!postID.equals(lastPostID)){
lastPostID=postID;
if(!postsWithKnownNonHeaderHeights.contains(postID))
continue; // We don't know full height of this post yet
int postHeight=0;
int heightOffset=0;
for(int j=holder.getAbsoluteAdapterPosition()-getMainAdapterOffset();j<displayItems.size();j++){
StatusDisplayItem item=displayItems.get(j);
if(!item.parentID.equals(postID))
break;
postHeight+=knownDisplayItemHeights.get(j+getMainAdapterOffset());
}
for(int j=holder.getAbsoluteAdapterPosition()-getMainAdapterOffset()-1;j>=0;j--){
StatusDisplayItem item=displayItems.get(j);
if(!item.parentID.equals(postID))
break;
int itemHeight=knownDisplayItemHeights.get(j+getMainAdapterOffset());
postHeight+=itemHeight;
heightOffset+=itemHeight;
}
int y=Math.round(child.getY())+postHeight/2-heightOffset;
Drawable check=selectedIDs.contains(postID) ? checkedIcon : uncheckedIcon;
c.save();
c.translate(V.dp(16), y-check.getIntrinsicHeight()/2f);
check.draw(c);
c.restore();
}
}
}
}
});
}
@Override
protected int getMainAdapterOffset(){
return 1;
}
@Override
protected RecyclerView.Adapter getAdapter(){
View headerView=getActivity().getLayoutInflater().inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
title.setText(R.string.report_choose_posts);
subtitle.setText(R.string.report_choose_posts_subtitle);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
}
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
tmpRect.offset(0, Math.round(child.getTranslationY()));
float y=tmpRect.bottom-V.dp(.5f);
paint.setAlpha(Math.round(255*child.getAlpha()));
c.drawLine(V.dp(16), y, parent.getWidth()-V.dp(16), y, paint);
}
private void onButtonClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putStringArrayList("statusIDs", selectedIDs);
args.putStringArrayList("ruleIDs", getArguments().getStringArrayList("ruleIDs"));
args.putString("reason", getArguments().getString("reason"));
Nav.go(getActivity(), ReportCommentFragment.class, args);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
}

View File

@@ -0,0 +1,127 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.reports.SendReport;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.ReportReason;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class ReportCommentFragment extends ToolbarFragment{
private String accountID;
private Account reportAccount;
private Button btn;
private View buttonBar;
private EditText commentEdit;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
E.register(this);
}
@Override
public void onDestroy(){
E.unregister(this);
super.onDestroy();
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
setTitle(getString(R.string.report_title, reportAccount.acct));
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_report_comment, container, false);
TextView title=view.findViewById(R.id.title);
TextView subtitle=view.findViewById(R.id.subtitle);
title.setText(R.string.report_comment_title);
subtitle.setText(R.string.report_comment_subtitle);
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
commentEdit=view.findViewById(R.id.text);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
private void onButtonClick(View v){
ReportReason reason=ReportReason.valueOf(getArguments().getString("reason"));
ArrayList<String> statusIDs=getArguments().getStringArrayList("statusIDs");
ArrayList<String> ruleIDs=getArguments().getStringArrayList("ruleIDs");
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, commentEdit.getText().toString(), false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", reason.name());
Nav.go(getActivity(), ReportDoneFragment.class, args);
buttonBar.postDelayed(()->E.post(new FinishReportFragmentsEvent(reportAccount.id)), 500);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.sending_report, false)
.exec(accountID);
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
}

View File

@@ -0,0 +1,158 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.reports.SendReport;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ReportReason;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ReportDoneFragment extends ToolbarFragment{
private String accountID;
private Account reportAccount;
private Button btn;
private View buttonBar;
private ReportReason reason;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reason=ReportReason.valueOf(getArguments().getString("reason"));
setTitle(getString(R.string.report_title, reportAccount.acct));
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_report_done, container, false);
TextView title=view.findViewById(R.id.title);
TextView subtitle=view.findViewById(R.id.subtitle);
if(reason==ReportReason.PERSONAL){
title.setText(R.string.report_personal_title);
subtitle.setText(R.string.report_personal_subtitle);
}else{
title.setText(R.string.report_sent_title);
subtitle.setText(getString(R.string.report_sent_subtitle, '@'+reportAccount.acct));
}
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
btn.setText(R.string.done);
if(reason!=ReportReason.PERSONAL){
View doneOverlay=view.findViewById(R.id.reported_overlay);
doneOverlay.setOutlineProvider(OutlineProviders.roundedRect(7));
ImageView ava=view.findViewById(R.id.avatar);
ava.setOutlineProvider(OutlineProviders.roundedRect(24));
ava.setClipToOutline(true);
ViewImageLoader.load(ava, null, new UrlImageLoaderRequest(reportAccount.avatar));
doneOverlay.setScaleX(1.5f);
doneOverlay.setScaleY(1.5f);
doneOverlay.setAlpha(0f);
doneOverlay.animate().scaleX(1f).scaleY(1f).alpha(1f).rotation(8.79f).setDuration(300).setStartDelay(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
}else{
view.findViewById(R.id.ava_reported).setVisibility(View.GONE);
}
TextView unfollowTitle=view.findViewById(R.id.unfollow_title);
TextView muteTitle=view.findViewById(R.id.mute_title);
TextView blockTitle=view.findViewById(R.id.block_title);
unfollowTitle.setText(getString(R.string.unfollow_user, '@'+reportAccount.acct));
muteTitle.setText(getString(R.string.mute_user, '@'+reportAccount.acct));
blockTitle.setText(getString(R.string.block_user, '@'+reportAccount.acct));
view.findViewById(R.id.unfollow_btn).setOnClickListener(v->onUnfollowClick());
view.findViewById(R.id.mute_btn).setOnClickListener(v->onMuteClick());
view.findViewById(R.id.block_btn).setOnClickListener(v->onBlockClick());
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
private void onButtonClick(View v){
Nav.finish(this);
}
private void onUnfollowClick(){
new SetAccountFollowed(reportAccount.id, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
Nav.finish(ReportDoneFragment.this);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
private void onMuteClick(){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
}
private void onBlockClick(){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
}
}

View File

@@ -0,0 +1,51 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.R;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.model.ReportReason;
import org.parceler.Parcels;
import me.grishka.appkit.Nav;
public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
@Override
protected Item getHeaderItem(){
return new Item(getString(R.string.report_choose_reason), getString(R.string.report_choose_reason_subtitle), null);
}
@Override
protected void populateItems(){
items.add(new Item(getString(R.string.report_reason_personal), getString(R.string.report_reason_personal_subtitle), ReportReason.PERSONAL.name()));
items.add(new Item(getString(R.string.report_reason_spam), getString(R.string.report_reason_spam_subtitle), ReportReason.SPAM.name()));
items.add(new Item(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
items.add(new Item(getString(R.string.report_reason_other), getString(R.string.report_reason_other_subtitle), ReportReason.OTHER.name()));
}
@Override
protected void onButtonClick(){
ReportReason reason=ReportReason.valueOf(selectedIDs.get(0));
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(reportStatus));
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", reason.name());
switch(reason){
case PERSONAL -> {
Nav.go(getActivity(), ReportDoneFragment.class, args);
content.postDelayed(()->Nav.finish(this), 500);
}
case SPAM, OTHER -> Nav.go(getActivity(), ReportAddPostsChoiceFragment.class, args);
case VIOLATION -> Nav.go(getActivity(), ReportRuleChoiceFragment.class, args);
}
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
}

View File

@@ -0,0 +1,48 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.model.Instance;
import org.parceler.Parcels;
import me.grishka.appkit.Nav;
public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
@Override
protected Item getHeaderItem(){
return new Item(getString(R.string.report_choose_rule), getString(R.string.report_choose_rule_subtitle), null);
}
@Override
protected void populateItems(){
isMultipleChoice=true;
Instance inst=AccountSessionManager.getInstance().getAccount(accountID).instance;
if(inst!=null && inst.rules!=null){
for(Instance.Rule rule:inst.rules){
items.add(new Item(rule.text, null, rule.id));
}
}
}
@Override
protected void onButtonClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(reportStatus));
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", getArguments().getString("reason"));
args.putStringArrayList("ruleIDs", selectedIDs);
Nav.go(getActivity(), ReportAddPostsChoiceFragment.class, args);
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
public enum ReportReason{
PERSONAL,
@SerializedName("spam")
SPAM,
@SerializedName("violation")
VIOLATION,
@SerializedName("other")
OTHER
}

View File

@@ -7,6 +7,8 @@ import android.view.View;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.function.Predicate;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -15,13 +17,21 @@ import me.grishka.appkit.utils.V;
public class DividerItemDecoration extends RecyclerView.ItemDecoration{
private Paint paint=new Paint();
private int paddingStart, paddingEnd;
private Predicate<RecyclerView.ViewHolder> drawDividerPredicate;
public static final Predicate<RecyclerView.ViewHolder> NOT_FIRST=vh->vh.getAbsoluteAdapterPosition()>0;
public DividerItemDecoration(Context context, @AttrRes int color, float thicknessDp, int paddingStartDp, int paddingEndDp){
this(context, color, thicknessDp, paddingStartDp, paddingEndDp, null);
}
public DividerItemDecoration(Context context, @AttrRes int color, float thicknessDp, int paddingStartDp, int paddingEndDp, Predicate<RecyclerView.ViewHolder> drawDividerPredicate){
paint.setColor(UiUtils.getThemeColor(context, color));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(thicknessDp));
paddingStart=V.dp(paddingStartDp);
paddingEnd=V.dp(paddingEndDp);
this.drawDividerPredicate=drawDividerPredicate;
}
@Override
@@ -33,7 +43,7 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(child);
if(pos<totalItems-1){
if(pos<totalItems-1 && (drawDividerPredicate==null || drawDividerPredicate.test(parent.getChildViewHolder(child)))){
float y=Math.round(child.getY()+child.getHeight()-paint.getStrokeWidth()/2f);
paint.setAlpha(Math.round(255f*child.getAlpha()));
c.drawLine(padLeft+child.getX(), y, child.getX()+child.getWidth()-padRight, y, paint);

View File

@@ -94,7 +94,7 @@ public class PhotoLayoutHelper{
};
}
}else if(cnt==3){
if(/*(ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) &&*/ orients.equals("www")){ // 2nd and 3rd photos are on the next line
if(/*(ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) &&*/ orients.equals("www") || true){ // 2nd and 3rd photos are on the next line
float hCover=Math.min(maxW/ratios.get(0), (maxH-marginH)*0.66f);
float w2=((maxW-marginW)/2);
float h=Math.min(maxH-hCover-marginH, Math.min(w2/ratios.get(1), w2/ratios.get(2)));

View File

@@ -19,6 +19,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
@@ -121,7 +122,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
name.setText(item.parsedName);
username.setText('@'+item.user.acct);
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
visibility.setVisibility(item.hasVisibilityToggle ? View.VISIBLE : View.GONE);
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
if(item.hasVisibilityToggle){
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
}
@@ -133,6 +134,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
extraText.setText(item.extraText);
}
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
avatar.setClickable(!item.inset);
}
@Override
@@ -178,7 +180,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}else if(id==R.id.block){
UiUtils.confirmToggleBlockUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, false, r->{});
}else if(id==R.id.report){
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
}
return true;
});