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

@ -10,7 +10,7 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 31
versionCode 6
versionCode 7
versionName "0.1"
}

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;
});

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray_800"/>
<corners android:radius="10dp"/>
<padding android:top="16dp" android:left="16dp" android:right="16dp" android:bottom="16dp"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray_25"/>
<corners android:radius="10dp"/>
<padding android:top="16dp" android:left="16dp" android:right="16dp" android:bottom="16dp"/>
</shape>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2zm3.22 6.97l-4.47 4.47-1.97-1.97c-0.293-0.293-0.767-0.293-1.06 0-0.293 0.293-0.293 0.767 0 1.06l2.5 2.5c0.293 0.293 0.767 0.293 1.06 0l5-5c0.293-0.293 0.293-0.767 0-1.06-0.293-0.293-0.767-0.293-1.06 0z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 22.002c5.524 0 10.002-4.478 10.002-10.001 0-5.524-4.478-10.002-10.002-10.002-5.524 0-10.002 4.478-10.002 10.002 0 5.523 4.478 10.001 10.002 10.001zm0-1.5c-4.695 0-8.502-3.806-8.502-8.501 0-4.696 3.807-8.502 8.502-8.502s8.502 3.806 8.502 8.502c0 4.695-3.807 8.501-8.502 8.501z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/ic_fluent_checkmark_circle_24_filled"/>
<item android:drawable="@drawable/ic_fluent_radio_button_24_regular"/>
</selector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="?android:colorBackground"/>
<stroke android:width="6.5dp" android:color="@color/success_600"/>
<corners android:radius="7dp"/>
<size android:width="205dp" android:height="69dp"/>
</shape>
</item>
<item android:drawable="@drawable/reported_text" android:gravity="center"/>
</layer-list>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:outlineProvider="bounds"
android:elevation="3dp"
tools:showIn="@layout/fragment_report_choice">
<Button
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
style="?primaryLargeButtonStyle"
android:text="@string/next" />
</FrameLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_wrap"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.grishka.appkit.views.UsableRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:clipToPadding="false"/>
<ViewStub android:layout="?emptyViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/empty"/>
</FrameLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.grishka.appkit.views.UsableRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?android:colorBackground"/>
<include layout="@layout/button_bar_one" />
</LinearLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/item_list_header"/>
<EditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:hint="@string/report_comment_hint"
android:inputType="textMultiLine|textCapSentences"
android:gravity="top|start"
android:minHeight="212dp"/>
</LinearLayout>
</ScrollView>
<include layout="@layout/button_bar_one"/>
</LinearLayout>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="32dp"
android:paddingBottom="8dp"
android:clipToPadding="false"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:gravity="center_vertical"
tools:text="Title"/>
<FrameLayout
android:id="@+id/ava_reported"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_marginBottom="24dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/avatar"
android:layout_width="104dp"
android:layout_height="104dp"
android:layout_gravity="center_horizontal"
tools:src="#0f0"/>
<View
android:id="@+id/reported_overlay"
android:layout_width="205dp"
android:layout_height="69dp"
android:layout_gravity="center"
android:elevation="2dp"
android:background="@drawable/reported_overlay"/>
</FrameLayout>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_medium"
android:textColor="?android:textColorSecondary"
tools:text="Subtitle"/>
<TextView
android:id="@+id/unfollow_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textAppearance="@style/m3_title_medium"
tools:text="@string/unfollow_user"/>
<Button
android:id="@+id/unfollow_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="?secondaryLargeButtonStyle"
android:text="@string/unfollow"/>
<TextView
android:id="@+id/mute_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textAppearance="@style/m3_title_medium"
tools:text="@string/mute_user"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:text="@string/mute_user_explain"/>
<Button
android:id="@+id/mute_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="?secondaryLargeButtonStyle"
android:text="@string/do_mute"/>
<TextView
android:id="@+id/block_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textAppearance="@style/m3_title_medium"
tools:text="@string/block_user"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:text="@string/block_user_explain"/>
<Button
android:id="@+id/block_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="?secondaryLargeButtonStyle"
android:text="@string/do_block"/>
</LinearLayout>
</ScrollView>
<include layout="@layout/button_bar_one"/>
</LinearLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/appkit_loader_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?android:colorBackground">
<include layout="@layout/appkit_toolbar"/>
<FrameLayout
android:id="@+id/appkit_loader_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<include layout="@layout/loading"
android:id="@+id/loading"/>
<ViewStub android:layout="?errorViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/error"
android:visibility="gone"/>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/content_stub"/>
</FrameLayout>
<include layout="@layout/button_bar_one"/>
</me.grishka.appkit.views.FragmentRootLinearLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="32dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:gravity="center_vertical"
tools:text="Title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_medium"
android:textColor="?android:textColorSecondary"
tools:text="Subtitle"/>
</LinearLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/checkbox"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:tint="?android:textColorSecondary"
android:src="@drawable/ic_round_checkbox"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/checkbox"
android:layout_marginStart="16dp"
android:textAppearance="@style/m3_title_medium"
tools:text="Title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/checkbox"
android:layout_below="@id/title"
android:layout_marginStart="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
tools:text="Subtitle"/>
</RelativeLayout>

View File

@ -16,6 +16,9 @@
<attr name="colorTabInactive" format="color"/>
<attr name="colorAccentLightest" format="color"/>
<attr name="primaryLargeButtonStyle" format="reference"/>
<attr name="secondaryLargeButtonStyle" format="reference"/>
<declare-styleable name="MaxWidthFrameLayout">
<attr name="android:maxWidth" format="dimension"/>
</declare-styleable>

View File

@ -145,4 +145,31 @@
<item quantity="one">Discussed %d time</item>
<item quantity="other">Discussed %d times</item>
</plurals>
<string name="report_title">Report %s</string>
<string name="report_choose_reason">Tell us what\'s going on with this post.</string>
<string name="report_choose_reason_subtitle">Choose the best match</string>
<string name="report_reason_personal">I don\'t like it</string>
<string name="report_reason_personal_subtitle">It is not something you want to see</string>
<string name="report_reason_spam">It\'s spam</string>
<string name="report_reason_spam_subtitle">Malicious links, fake engagement, or repetitive replies</string>
<string name="report_reason_violation">It violates server rules</string>
<string name="report_reason_violation_subtitle">You are aware that it breaks specific rules</string>
<string name="report_reason_other">It\'s something else</string>
<string name="report_reason_other_subtitle">The issue does not fit into other categories</string>
<string name="report_choose_rule">Which rules are being violated?</string>
<string name="report_choose_rule_subtitle">Select all that apply</string>
<string name="report_choose_posts">Are there any posts that back up this report?</string>
<string name="report_choose_posts_subtitle">Optional. Select all that apply</string>
<string name="report_comment_title">Is there anything else you think we should know?</string>
<string name="report_comment_subtitle">Optional.</string>
<string name="report_comment_hint">Additional comments</string>
<string name="sending_report">Sending report…</string>
<string name="report_sent_title">Thanks for reporting, we\'ll look into this.</string>
<string name="report_sent_subtitle">While we review this, you can take action against %s.</string>
<string name="unfollow_user">Unfollow %s</string>
<string name="unfollow">Unfollow</string>
<string name="mute_user_explain">You won\'t see their posts or reblogs in your home feed. They won\'t know they\'ve been muted.</string>
<string name="block_user_explain">They will no longer be able to follow or see your posts, but thye can see if they\'ve been blocked.</string>
<string name="report_personal_title">Don\'t want to see this?</string>
<string name="report_personal_subtitle">When you see something you don\'t like on Mastodon, you can remove the person from your experience.</string>
</resources>

View File

@ -7,9 +7,12 @@
<item name="appkitBackDrawable">@drawable/ic_fluent_arrow_left_24_regular</item>
<item name="android:splitMotionEvents">false</item>
<item name="android:windowBackground">?colorWindowBackground</item>
<item name="android:editTextStyle">@style/Widget.Mastodon.EditText</item>
<item name="android:buttonStyle">@style/Widget.Mastodon.Button.Primary_DarkOnLight</item>
<item name="secondaryButtonStyle">@style/Widget.Mastodon.Button.Secondary_DarkOnLight</item>
<item name="primaryLargeButtonStyle">@style/Widget.Mastodon.Button.Large.Primary_DarkOnLight</item>
<item name="secondaryLargeButtonStyle">@style/Widget.Mastodon.Button.Large.Secondary_DarkOnLight</item>
<item name="android:colorAccent">@color/primary_700</item>
<item name="android:colorPrimary">@color/gray_800</item>
<item name="android:colorBackground">@color/gray_100</item>
@ -34,6 +37,7 @@
<item name="colorAccentLightest">@color/primary_100</item>
<item name="buttonBackground">@drawable/bg_button_primary_dark_on_light</item>
<item name="android:editTextBackground">@drawable/bg_edittext_light</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
@ -46,9 +50,12 @@
<item name="appkitBackDrawable">@drawable/ic_fluent_arrow_left_24_regular</item>
<item name="android:splitMotionEvents">false</item>
<item name="android:windowBackground">?colorWindowBackground</item>
<item name="android:editTextStyle">@style/Widget.Mastodon.EditText</item>
<item name="android:buttonStyle">@style/Widget.Mastodon.Button.Primary_LightOnDark</item>
<item name="secondaryButtonStyle">@style/Widget.Mastodon.Button.Secondary_LightOnDark</item>
<item name="primaryLargeButtonStyle">@style/Widget.Mastodon.Button.Large.Primary_LightOnDark</item>
<item name="secondaryLargeButtonStyle">@style/Widget.Mastodon.Button.Large.Secondary_LightOnDark</item>
<item name="android:colorAccent">@color/primary_600</item>
<item name="android:colorPrimary">@color/gray_50</item>
<item name="android:colorBackground">@color/gray_700</item>
@ -75,6 +82,7 @@
<item name="colorAccentLightest">@color/primary_100</item>
<item name="buttonBackground">@drawable/bg_button_primary_light_on_dark</item>
<item name="android:editTextBackground">@drawable/bg_edittext_light</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">false</item>
@ -114,6 +122,13 @@
<item name="android:paddingRight">16dp</item>
</style>
<style name="Widget.Mastodon.Button.Large">
<item name="android:minHeight">56dp</item>
<item name="android:paddingLeft">32dp</item>
<item name="android:paddingRight">32dp</item>
<item name="android:textAppearance">@style/m3_title_medium</item>
</style>
<style name="Widget.Mastodon.Button.Primary_DarkOnLight">
<item name="android:background">@drawable/bg_button_primary_dark_on_light</item>
<item name="android:textColor">@color/button_text_primary_dark_on_light</item>
@ -134,6 +149,32 @@
<item name="android:textColor">@color/button_text_secondary_light_on_dark</item>
</style>
<style name="Widget.Mastodon.Button.Large.Primary_DarkOnLight">
<item name="android:background">@drawable/bg_button_primary_dark_on_light</item>
<item name="android:textColor">@color/button_text_primary_dark_on_light</item>
</style>
<style name="Widget.Mastodon.Button.Large.Secondary_DarkOnLight">
<item name="android:background">@drawable/bg_button_secondary_dark_on_light</item>
<item name="android:textColor">@color/button_text_secondary_dark_on_light</item>
</style>
<style name="Widget.Mastodon.Button.Large.Primary_LightOnDark">
<item name="android:background">@drawable/bg_button_primary_light_on_dark</item>
<item name="android:textColor">@color/button_text_primary_light_on_dark</item>
</style>
<style name="Widget.Mastodon.Button.Large.Secondary_LightOnDark">
<item name="android:background">@drawable/bg_button_secondary_light_on_dark</item>
<item name="android:textColor">@color/button_text_secondary_light_on_dark</item>
</style>
<style name="Widget.Mastodon.EditText" parent="android:Widget.Material.Light.EditText">
<item name="android:textColorHint">?android:textColorSecondary</item>
<item name="android:elevation">2dp</item>
<item name="android:textAppearance">@style/m3_body_large</item>
</style>
<style name="Theme.Mastodon.Dialog.Alert" parent="android:Theme.Material.Light.Dialog.Alert">
<item name="android:windowTitleStyle">@style/alert_title</item>
<item name="android:dialogPreferredPadding">24dp</item>
@ -188,6 +229,7 @@
<style name="m3_body_medium">
<item name="android:textSize">14dp</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:lineSpacingExtra">4dp</item>
</style>
<style name="m3_title_medium">
@ -226,4 +268,10 @@
<item name="android:textSize">24dp</item>
<item name="android:textColor">?android:textColorPrimary</item>
</style>
<style name="m3_headline_medium">
<item name="android:textSize">28dp</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:lineSpacingExtra">3dp</item>
</style>
</resources>