Reporting M3 redesign

This commit is contained in:
Grishka
2023-05-22 17:08:04 +03:00
parent 34a2af8429
commit bd7157c172
75 changed files with 2371 additions and 488 deletions

View File

@@ -5,14 +5,9 @@ import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -31,18 +26,15 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
@@ -268,7 +260,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
list.getDecoratedBoundsWithMargins(view, outRect);
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
list.getDecoratedBoundsWithMargins(view, outRect);
}else{
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
}
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
@@ -430,6 +426,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
}
list.invalidateItemDecorations();
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
@@ -557,6 +554,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return attachmentViewsPool;
}
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){
@@ -566,7 +565,9 @@ 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 & (~0x80000000)], getActivity(), parent);
BindableViewHolder<StatusDisplayItem> holder=(BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent);
onModifyItemViewHolder(holder);
return holder;
}
@Override

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status;
@@ -34,4 +35,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
})
.exec(accountID);
}
@Override
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op
}
}

View File

@@ -584,6 +584,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
args.putParcelable("relationship", Parcels.wrap(relationship));
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url);

View File

@@ -108,13 +108,7 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
contentView.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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(contentView, insets));
}
@Override

View File

@@ -151,13 +151,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void loadServerPrivacyPolicy(){

View File

@@ -20,6 +20,7 @@ import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -330,13 +331,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override

View File

@@ -111,13 +111,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{

View File

@@ -130,13 +130,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override

View File

@@ -161,13 +161,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private View makeFieldsRow(){

View File

@@ -366,13 +366,7 @@ public class SignupFragment extends ToolbarFragment{
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void onGoBackLinkClick(LinkSpan span){

View File

@@ -1,15 +1,13 @@
package org.joinmastodon.android.fragments.report;
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.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.E;
@@ -23,14 +21,9 @@ 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 MastodonToolbarFragment{
@@ -38,12 +31,13 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
private MergeRecyclerAdapter adapter;
private Button btn;
private View buttonBar;
protected ArrayList<Item> items=new ArrayList<>();
protected ArrayList<ChoiceItem> items=new ArrayList<>();
protected boolean isMultipleChoice;
protected ArrayList<String> selectedIDs=new ArrayList<>();
protected String accountID;
protected Account reportAccount;
protected Status reportStatus;
protected ProgressBar progressBar;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -75,122 +69,33 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
populateItems();
Item header=getHeaderItem();
ChoiceItem 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);
TextView stepCounter=headerView.findViewById(R.id.step_counter);
title.setText(header.title);
subtitle.setText(header.subtitle);
stepCounter.setText(getString(R.string.step_x_of_n, getStepNumber(), 3));
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);
progressBar=view.findViewById(R.id.top_progress);
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ChoiceItemsAdapter(getActivity(), isMultipleChoice, items, list, selectedIDs, btn::setEnabled));
list.setAdapter(adapter);
return view;
}
protected abstract Item getHeaderItem();
protected abstract ChoiceItem getHeaderItem();
protected abstract void populateItems();
protected abstract void onButtonClick();
protected abstract int getStepNumber();
@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 ivh && ivh.getItem().id.equals(prev)){
ivh.rebind();
break;
}
}
}
selectedIDs.add(item.id);
rebind();
}
}
btn.setEnabled(!selectedIDs.isEmpty());
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.fragments.report;
class ChoiceItem{
public String title, subtitle, id;
public ChoiceItem(String title, String subtitle, String id){
this.title=title;
this.subtitle=subtitle;
this.id=id;
}
}

View File

@@ -0,0 +1,85 @@
package org.joinmastodon.android.fragments.report;
import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
import java.util.ArrayList;
import java.util.function.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
class ChoiceItemViewHolder extends BindableViewHolder<ChoiceItem> implements UsableRecyclerView.Clickable{
private final TextView title, subtitle;
private final View checkbox;
private final CheckableLinearLayout view;
private final boolean isMultipleChoice;
private final RecyclerView list;
private final ArrayList<String> selectedIDs;
private final Consumer<Boolean> buttonEnabledSetter;
public ChoiceItemViewHolder(Context context, boolean isMultipleChoice, RecyclerView list, ArrayList<String> selectedIDs, Consumer<Boolean> buttonEnabledSetter){
super(context, R.layout.item_report_choice, list);
this.buttonEnabledSetter=buttonEnabledSetter;
this.isMultipleChoice=isMultipleChoice;
this.list=list;
this.selectedIDs=selectedIDs;
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
checkbox=findViewById(R.id.checkbox);
CompoundButton cb=isMultipleChoice ? new CheckBox(context) : new RadioButton(context);
checkbox.setBackground(cb.getButtonDrawable());
view=(CheckableLinearLayout) itemView;
}
@Override
public void onBind(ChoiceItem item){
title.setText(item.title);
if(TextUtils.isEmpty(item.subtitle)){
subtitle.setVisibility(View.GONE);
view.setMinimumHeight(V.dp(56));
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.subtitle);
view.setMinimumHeight(V.dp(72));
}
view.setChecked(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 ChoiceItemViewHolder ivh && ivh.getItem().id.equals(prev)){
ivh.rebind();
break;
}
}
}
selectedIDs.add(item.id);
rebind();
}
}
buttonEnabledSetter.accept(!selectedIDs.isEmpty());
}
}

View File

@@ -0,0 +1,46 @@
package org.joinmastodon.android.fragments.report;
import android.content.Context;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.function.Consumer;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.views.UsableRecyclerView;
class ChoiceItemsAdapter extends RecyclerView.Adapter<ChoiceItemViewHolder>{
private final Context context;
private final boolean isMultipleChoice;
private final ArrayList<ChoiceItem> items;
private final RecyclerView list;
private final ArrayList<String> selectedIDs;
private final Consumer<Boolean> buttonEnabledSetter;
public ChoiceItemsAdapter(Context context, boolean isMultipleChoice, ArrayList<ChoiceItem> items, RecyclerView list, ArrayList<String> selectedIDs, Consumer<Boolean> buttonEnabledSetter){
this.context=context;
this.isMultipleChoice=isMultipleChoice;
this.items=items;
this.list=list;
this.selectedIDs=selectedIDs;
this.buttonEnabledSetter=buttonEnabledSetter;
}
@NonNull
@Override
public ChoiceItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ChoiceItemViewHolder(context, isMultipleChoice, list, selectedIDs, buttonEnabledSetter);
}
@Override
public void onBindViewHolder(@NonNull ChoiceItemViewHolder holder, int position){
holder.bind(items.get(position));
}
@Override
public int getItemCount(){
return items.size();
}
}

View File

@@ -4,13 +4,11 @@ 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.ProgressBar;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
@@ -22,37 +20,34 @@ import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
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 androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
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){
@@ -72,13 +67,15 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@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)
if(reportStatus!=null){
selectedIDs.add(reportStatus.id);
setTitle(getString(R.string.report_title, reportAccount.acct));
setTitle(R.string.report_title_post);
}else{
setTitle(getString(R.string.report_title, reportAccount.acct));
}
loadData();
}
@@ -88,6 +85,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
for(Status s:result){
s.sensitive=true;
}
onDataLoaded(result, !result.isEmpty());
}
})
@@ -100,8 +100,10 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
selectedIDs.remove(id);
else
selectedIDs.add(id);
list.invalidate();
btn.setEnabled(!selectedIDs.isEmpty());
CheckableHeaderStatusDisplayItem.Holder holder=findHolderOfType(id, CheckableHeaderStatusDisplayItem.Holder.class);
if(holder!=null)
holder.rebind();
}
@Override
@@ -110,88 +112,27 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(this::onButtonClick);
view.findViewById(R.id.btn_back).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)
if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder)
return;
outRect.left=V.dp(40);
if(holder instanceof AudioStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
}else if(holder instanceof LinkCardStatusDisplayItem.Holder){
}else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.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 sdiHolder){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
String id=sdiHolder.getItemID();
int height=tmpRect.height();
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<?> sdiHolder){
String postID=sdiHolder.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();
}
}
}
}
});
ProgressBar topProgress=view.findViewById(R.id.top_progress);
topProgress.setProgress(getArguments().containsKey("ruleIDs") ? 50 : 33);
}
@Override
@@ -204,10 +145,8 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
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);
TextView stepCounter=headerView.findViewById(R.id.step_counter);
title.setText(R.string.report_choose_posts);
subtitle.setText(R.string.report_choose_posts_subtitle);
stepCounter.setText(getString(R.string.step_x_of_n, 2, 3));
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
@@ -216,17 +155,14 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
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));
if(reportStatus!=null)
args.putBoolean("fromPost", true);
if(v.getId()==R.id.btn_next){
args.putStringArrayList("statusIDs", selectedIDs);
}else{
@@ -237,18 +173,13 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
args.putStringArrayList("ruleIDs", getArguments().getStringArrayList("ruleIDs"));
args.putString("reason", getArguments().getString("reason"));
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Subscribe
@@ -261,4 +192,32 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
protected boolean wantsOverlaySystemNavigation(){
return false;
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
}
@Override
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){
if((Object)holder instanceof MediaGridStatusDisplayItem.Holder h){
View layout=h.getLayout();
layout.setOutlineProvider(OutlineProviders.roundedRect(8));
layout.setClipToOutline(true);
View overlay=h.getSensitiveOverlay();
overlay.setOutlineProvider(OutlineProviders.roundedRect(8));
overlay.setClipToOutline(true);
}else if((Object)holder instanceof CheckableHeaderStatusDisplayItem.Holder h){
h.setIsChecked(this::isChecked);
}
}
private boolean isChecked(CheckableHeaderStatusDisplayItem.Holder holder){
return selectedIDs.contains(holder.getItem().parentID);
}
}

View File

@@ -1,14 +1,16 @@
package org.joinmastodon.android.fragments.report;
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.EditText;
import android.widget.ProgressBar;
import android.widget.Switch;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
@@ -16,6 +18,7 @@ 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.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
import org.joinmastodon.android.model.Account;
@@ -28,8 +31,6 @@ 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 MastodonToolbarFragment{
private String accountID;
@@ -37,6 +38,8 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
private Button btn;
private View buttonBar;
private EditText commentEdit;
private Switch forwardSwitch;
private View forwardBtn;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -54,10 +57,12 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
@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));
if(getArguments().getBoolean("fromPost", false))
setTitle(R.string.report_title_post);
else
setTitle(getString(R.string.report_title, reportAccount.acct));
}
@@ -67,16 +72,23 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
TextView title=view.findViewById(R.id.title);
TextView subtitle=view.findViewById(R.id.subtitle);
TextView stepCounter=view.findViewById(R.id.step_counter);
title.setText(R.string.report_comment_title);
subtitle.setVisibility(View.GONE);
stepCounter.setText(getString(R.string.step_x_of_n, 3, 3));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(this::onButtonClick);
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
commentEdit=view.findViewById(R.id.text);
forwardSwitch=view.findViewById(R.id.forward_switch);
forwardBtn=view.findViewById(R.id.forward_report);
forwardBtn.setOnClickListener(v->forwardSwitch.toggle());
String myDomain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(!TextUtils.isEmpty(reportAccount.getDomain()) && !myDomain.equalsIgnoreCase(reportAccount.getDomain())){
TextView forwardTitle=view.findViewById(R.id.forward_title);
forwardTitle.setText(getString(R.string.forward_report_to_server, reportAccount.getDomain()));
}else{
forwardBtn.setVisibility(View.GONE);
}
return view;
}
@@ -84,25 +96,21 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
ProgressBar topProgress=view.findViewById(R.id.top_progress);
topProgress.setProgress(getArguments().containsKey("ruleIDs") ? 75 : 66);
}
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
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, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), true)
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), forwardSwitch.isChecked())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
@@ -110,6 +118,8 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", reason.name());
args.putBoolean("fromPost", getArguments().getBoolean("fromPost", false));
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
Nav.go(getActivity(), ReportDoneFragment.class, args);
buttonBar.postDelayed(()->E.post(new FinishReportFragmentsEvent(reportAccount.id)), 500);
}

View File

@@ -1,12 +1,14 @@
package org.joinmastodon.android.fragments.report;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Build;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.animation.PathInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
@@ -23,10 +25,13 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import androidx.annotation.DrawableRes;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
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;
@@ -38,6 +43,13 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
private Button btn;
private View buttonBar;
private ReportReason reason;
private TextView unfollowTitle;
private TextView muteTitle;
private TextView blockTitle;
private View unfollowBtn;
private View muteBtn;
private View blockBtn;
private Relationship relationship;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -52,10 +64,13 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reason=ReportReason.valueOf(getArguments().getString("reason"));
setTitle(getString(R.string.report_title, reportAccount.acct));
relationship=Parcels.unwrap(getArguments().getParcelable("relationship"));
if(getArguments().getBoolean("fromPost", false))
setTitle(R.string.report_title_post);
else
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);
@@ -64,10 +79,18 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
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);
if(relationship!=null && relationship.blocking){
subtitle.setText(R.string.report_personal_already_blocked);
}else{
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));
if(relationship!=null && relationship.blocking){
subtitle.setText(R.string.report_sent_already_blocked);
}else{
subtitle.setText(getString(R.string.report_sent_subtitle, '@'+reportAccount.acct));
}
}
btn=view.findViewById(R.id.btn_next);
@@ -76,31 +99,65 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
btn.setText(R.string.done);
if(reason!=ReportReason.PERSONAL){
View doneOverlay=view.findViewById(R.id.reported_overlay);
doneOverlay.setOutlineProvider(OutlineProviders.roundedRect(7));
TextView stamp=view.findViewById(R.id.reported_stamp);
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();
stamp.setAlpha(0f);
stamp.setTranslationX(V.dp(148));
stamp.setTranslationY(V.dp(-296));
stamp.setScaleX(3.5f);
stamp.setScaleY(3.5f);
ObjectAnimator alpha=ObjectAnimator.ofFloat(stamp, View.ALPHA, 1f).setDuration(400);
alpha.setInterpolator(new PathInterpolator(0.16f, 1, 0.3f, 1));
alpha.start();
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.TRANSLATION_X, 0f));
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.TRANSLATION_Y, 0f));
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.SCALE_X, 1f));
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.SCALE_Y, 1f));
}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=view.findViewById(R.id.unfollow_title);
muteTitle=view.findViewById(R.id.mute_title);
blockTitle=view.findViewById(R.id.block_title);
unfollowBtn=view.findViewById(R.id.unfollow_btn);
muteBtn=view.findViewById(R.id.mute_btn);
blockBtn=view.findViewById(R.id.block_btn);
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));
setIconToButton(R.drawable.ic_person_remove_20px, unfollowTitle);
setIconToButton(R.drawable.ic_block_20px, blockTitle);
setIconToButton(R.drawable.ic_volume_off_20px, muteTitle);
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());
unfollowBtn.setOnClickListener(v->onUnfollowClick());
muteBtn.setOnClickListener(v->onMuteClick());
blockBtn.setOnClickListener(v->onBlockClick());
if(relationship!=null){
if(relationship.blocking){
muteBtn.setVisibility(View.GONE);
view.findViewById(R.id.mute_explanation).setVisibility(View.GONE);
unfollowBtn.setVisibility(View.GONE);
view.findViewById(R.id.unfollow_explanation).setVisibility(View.GONE);
blockBtn.setVisibility(View.GONE);
view.findViewById(R.id.block_explanation).setVisibility(View.GONE);
}else{
if(relationship.muting){
muteBtn.setVisibility(View.GONE);
view.findViewById(R.id.mute_explanation).setVisibility(View.GONE);
}
if(!relationship.following){
unfollowBtn.setVisibility(View.GONE);
view.findViewById(R.id.unfollow_explanation).setVisibility(View.GONE);
}
}
}
return view;
}
@@ -108,18 +165,11 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
@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()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void onButtonClick(View v){
@@ -131,8 +181,13 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
Nav.finish(ReportDoneFragment.this);
E.post(new RemoveAccountPostsEvent(accountID, reportAccount.id, true));
unfollowTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
unfollowTitle.setText(getString(R.string.unfollowed_user, '@'+reportAccount.acct));
setIconToButton(R.drawable.ic_check_24px, unfollowTitle);
unfollowBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
unfollowBtn.setClickable(false);
unfollowBtn.setFocusable(false);
}
@Override
@@ -145,10 +200,50 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
}
private void onMuteClick(){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
UiUtils.confirmToggleMuteUser(getActivity(), accountID, reportAccount, false, rel->{
muteTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
muteTitle.setText(getString(R.string.muted_user, '@'+reportAccount.acct));
setIconToButton(R.drawable.ic_check_24px, muteTitle);
muteBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
muteBtn.setClickable(false);
muteBtn.setFocusable(false);
});
}
private void onBlockClick(){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
UiUtils.confirmToggleBlockUser(getActivity(), accountID, reportAccount, false, rel->{
blockTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
blockTitle.setText(getString(R.string.blocked_user, '@'+reportAccount.acct));
setIconToButton(R.drawable.ic_check_24px, blockTitle);
blockBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
blockBtn.setClickable(false);
blockBtn.setFocusable(false);
if(unfollowBtn.isClickable())
unfollowBtn.setEnabled(false);
if(muteBtn.isClickable())
muteBtn.setEnabled(false);
});
}
@Override
protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_close_24;
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
private void setIconToButton(@DrawableRes int icon, TextView button){
Drawable d=getResources().getDrawable(icon, getActivity().getTheme()).mutate();
d.setBounds(0, 0, V.dp(18), V.dp(18));
d.setTintList(button.getTextColors());
button.setCompoundDrawablesRelative(d, null, null, null);
}
private void setupSpringAnimation(SpringAnimation anim){
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY).setStiffness(500);
anim.start();
}
}

View File

@@ -1,36 +1,106 @@
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ReportReason;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import me.grishka.appkit.Nav;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
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 class ReportReasonChoiceFragment extends StatusListFragment{
private MergeRecyclerAdapter mergeAdapter;
private Button btn;
private View buttonBar;
protected ArrayList<ChoiceItem> items=new ArrayList<>();
protected boolean isMultipleChoice;
protected ArrayList<String> selectedIDs=new ArrayList<>();
protected Account reportAccount;
protected Status reportStatus;
protected ProgressBar progressBar;
private Relationship relationship;
public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
@Override
protected Item getHeaderItem(){
return new Item(reportStatus!=null ? getString(R.string.report_choose_reason) : getString(R.string.report_choose_reason_account, reportAccount.acct), getString(R.string.report_choose_reason_subtitle), null);
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setListLayoutId(R.layout.fragment_content_report_posts);
setLayout(R.layout.fragment_report_posts);
E.register(this);
}
@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()));
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"));
if(reportStatus!=null){
Status hiddenStatus=new Status(reportStatus);
hiddenStatus.spoilerText=getString(R.string.post_hidden);
onDataLoaded(Collections.singletonList(hiddenStatus));
setTitle(R.string.report_title_post);
}else{
onDataLoaded(Collections.emptyList());
setTitle(getString(R.string.report_title, reportAccount.acct));
}
relationship=Parcels.unwrap(getArguments().getParcelable("relationship"));
if(relationship==null && reportStatus==null)
loadRelationships(Collections.singleton(reportAccount.id));
items.add(new ChoiceItem(getString(R.string.report_reason_personal), getString(R.string.report_reason_personal_subtitle), ReportReason.PERSONAL.name()));
items.add(new ChoiceItem(getString(R.string.report_reason_spam), getString(R.string.report_reason_spam_subtitle), ReportReason.SPAM.name()));
Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
if(inst!=null && inst.rules!=null && !inst.rules.isEmpty()){
items.add(new Item(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
items.add(new ChoiceItem(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()));
items.add(new ChoiceItem(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();
@@ -38,6 +108,8 @@ public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
args.putParcelable("status", Parcels.wrap(reportStatus));
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", reason.name());
args.putBoolean("fromPost", reportStatus!=null);
args.putParcelable("relationship", Parcels.wrap(relationship));
switch(reason){
case PERSONAL -> {
Nav.go(getActivity(), ReportDoneFragment.class, args);
@@ -48,14 +120,146 @@ public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
}
}
@Override
protected int getStepNumber(){
return 1;
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected void doLoadData(int offset, int count){
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
LayoutInflater inflater=getActivity().getLayoutInflater();
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(reportStatus!=null ? getString(R.string.report_choose_reason) : getString(R.string.report_choose_reason_account, reportAccount.acct));
subtitle.setText(getString(R.string.report_choose_reason_subtitle));
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(super.getAdapter());
adapter.addAdapter(new ChoiceItemsAdapter(getActivity(), isMultipleChoice, items, list, selectedIDs, btn::setEnabled));
return mergeAdapter=adapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
progressBar=view.findViewById(R.id.top_progress);
progressBar.setProgress(5);
super.onViewCreated(view, savedInstanceState);
((UsableRecyclerView)list).setIncludeMarginsInItemHitbox(false);
if(reportStatus!=null){
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@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 instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
outRect.left=V.dp(16);
outRect.right=V.dp(16);
}
}
});
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
int firstPos=list.getChildAdapterPosition(list.getChildAt(0));
int lastPos=-1;
for(int i=list.getChildCount()-1;i>=0;i--){
lastPos=list.getChildAdapterPosition(list.getChildAt(i));
if(lastPos!=-1)
break;
}
int postStart=mergeAdapter.getPositionForAdapter(adapter);
if(lastPos<postStart || firstPos>postStart+displayItems.size()){
return;
}
float top=V.dp(-12);
float bottom=parent.getHeight()+V.dp(12);
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(child);
if(pos==postStart)
top=child.getY();
if(pos==postStart+displayItems.size())
bottom=child.getY()-V.dp(16);
}
float off=paint.getStrokeWidth()/2f;
c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), paint);
}
@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 instanceof StatusDisplayItem.Holder<?>){
outRect.left=outRect.right=V.dp(16);
}
int index=holder.getAbsoluteAdapterPosition()-mergeAdapter.getPositionForAdapter(adapter);
if(index==displayItems.size()){
outRect.top=V.dp(32);
}
}
});
}
}
@Override
protected boolean wantsOverlaySystemNavigation(){
return false;
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER);
}
@Override
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){
if((Object)holder instanceof MediaGridStatusDisplayItem.Holder h){
View layout=h.getLayout();
layout.setOutlineProvider(OutlineProviders.roundedRect(8));
layout.setClipToOutline(true);
View overlay=h.getSensitiveOverlay();
overlay.setOutlineProvider(OutlineProviders.roundedRect(8));
overlay.setClipToOutline(true);
}
}
@Override
public void putRelationship(String id, Relationship rel){
super.putRelationship(id, rel);
if(id.equals(reportAccount.id))
relationship=rel;
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.report;
import android.os.Bundle;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -14,8 +15,8 @@ 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);
protected ChoiceItem getHeaderItem(){
return new ChoiceItem(getString(R.string.report_choose_rule), getString(R.string.report_choose_rule_subtitle), null);
}
@Override
@@ -24,7 +25,7 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
if(inst!=null && inst.rules!=null){
for(Instance.Rule rule:inst.rules){
items.add(new Item(rule.text, null, rule.id));
items.add(new ChoiceItem(rule.text, null, rule.id));
}
}
}
@@ -37,17 +38,19 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", getArguments().getString("reason"));
args.putStringArrayList("ruleIDs", selectedIDs);
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
Nav.go(getActivity(), ReportAddPostsChoiceFragment.class, args);
}
@Override
protected int getStepNumber(){
return 1;
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
progressBar.setProgress(25);
}
}

View File

@@ -1,7 +1,9 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
@Parcel
public class Relationship extends BaseModel{
@RequiredField
public String id;

View File

@@ -59,6 +59,41 @@ public class Status extends BaseModel implements DisplayItemsParent{
public transient boolean hasGapAfter;
private transient String strippedText;
public Status(){}
public Status(Status other){
this.id=other.id;
this.uri=other.uri;
this.createdAt=other.createdAt;
this.account=other.account;
this.content=other.content;
this.visibility=other.visibility;
this.sensitive=other.sensitive;
this.spoilerText=other.spoilerText;
this.mediaAttachments=other.mediaAttachments;
this.application=other.application;
this.mentions=other.mentions;
this.tags=other.tags;
this.emojis=other.emojis;
this.reblogsCount=other.reblogsCount;
this.favouritesCount=other.favouritesCount;
this.repliesCount=other.repliesCount;
this.editedAt=other.editedAt;
this.url=other.url;
this.inReplyToId=other.inReplyToId;
this.inReplyToAccountId=other.inReplyToAccountId;
this.reblog=other.reblog;
this.poll=other.poll;
this.card=other.card;
this.language=other.language;
this.text=other.text;
this.favourited=other.favourited;
this.reblogged=other.reblogged;
this.muted=other.muted;
this.bookmarked=other.bookmarked;
this.pinned=other.pinned;
}
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();

View File

@@ -0,0 +1,51 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
import java.time.Instant;
import java.util.function.Predicate;
public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
public CheckableHeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
super(parentID, user, createdAt, parentFragment, accountID, status, extraText);
}
@Override
public Type getType(){
return Type.HEADER_CHECKABLE;
}
public static class Holder extends HeaderStatusDisplayItem.Holder{
private final View checkbox;
private final CheckableRelativeLayout view;
private Predicate<Holder> isChecked;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_header_checkable, parent);
checkbox=findViewById(R.id.checkbox);
view=(CheckableRelativeLayout) itemView;
checkbox.setBackground(new CheckBox(activity).getButtonDrawable());
}
@Override
public void onBind(HeaderStatusDisplayItem item){
super.onBind(item);
if(isChecked!=null){
view.setChecked(isChecked.test(this));
}
}
public void setIsChecked(Predicate<Holder> isChecked){
this.isChecked=isChecked;
}
}
}

View File

@@ -3,18 +3,16 @@ package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.graphics.Outline;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
@@ -43,6 +41,7 @@ import java.time.Instant;
import java.util.Collections;
import java.util.List;
import androidx.annotation.LayoutRes;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
@@ -114,7 +113,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private APIRequest<?> currentRelationshipRequest;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_header, parent);
this(activity, R.layout.display_item_header, parent);
}
protected Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
super(activity, layout, parent);
name=findViewById(R.id.name);
timeAndUsername=findViewById(R.id.time_and_username);
avatar=findViewById(R.id.avatar);
@@ -165,6 +168,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
args.putParcelable("relationship", Parcels.wrap(relationship));
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(activity, item.status.url);

View File

@@ -48,6 +48,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
public final Status status;
public boolean sensitiveRevealed;
public String sensitiveTitle;
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
super(parentID, parentFragment);
@@ -103,6 +104,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final LayerDrawable sensitiveOverlayBG;
private static final ColorDrawable drawableForWhenThereIsNoBlurhash=new ColorDrawable(0xffffffff);
private final TextView hideSensitiveButton;
private final TextView sensitiveText;
private int altTextIndex=-1;
private Animator altTextAnimator;
@@ -112,7 +114,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
wrapper=(FrameLayout)itemView;
layout=new MediaGridLayout(activity);
wrapper.addView(layout);
wrapper.setPadding(0, 0, 0, V.dp(8));
wrapper.setClipToPadding(false);
overlays=new MaxWidthFrameLayout(activity);
@@ -136,15 +137,20 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
activity.getLayoutInflater().inflate(R.layout.overlay_image_sensitive, overlays);
sensitiveOverlay=findViewById(R.id.sensitive_overlay);
sensitiveOverlayBG=(LayerDrawable) sensitiveOverlay.getBackground();
sensitiveOverlayBG=(LayerDrawable) sensitiveOverlay.getBackground().mutate();
sensitiveOverlayBG.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(false));
sensitiveOverlayBG.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(true));
sensitiveOverlay.setBackground(sensitiveOverlayBG);
sensitiveOverlay.setOnClickListener(v->revealSensitive());
hideSensitiveButton.setOnClickListener(v->hideSensitive());
sensitiveText=findViewById(R.id.sensitive_text);
}
@Override
public void onBind(MediaGridStatusDisplayItem item){
wrapper.setPadding(0, 0, 0, item.inset ? 0 : V.dp(8));
if(altTextAnimator!=null)
altTextAnimator.cancel();
@@ -190,6 +196,10 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
layout.setVisibility(View.VISIBLE);
}
hideSensitiveButton.setVisibility(item.status.sensitive ? View.VISIBLE : View.GONE);
if(!TextUtils.isEmpty(item.sensitiveTitle))
sensitiveText.setText(item.sensitiveTitle);
else
sensitiveText.setText(R.string.sensitive_content_explain);
}
@Override
@@ -346,5 +356,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
item.sensitiveRevealed=false;
V.setVisibilityAnimated(sensitiveOverlay, View.VISIBLE, ()->layout.setVisibility(View.INVISIBLE));
}
public MediaGridLayout getLayout(){
return layout;
}
public View getSensitiveOverlay(){
return sensitiveOverlay;
}
}
}

View File

@@ -33,6 +33,11 @@ public abstract class StatusDisplayItem{
public boolean inset;
public int index;
public static final int FLAG_INSET=1;
public static final int FLAG_NO_FOOTER=1 << 1;
public static final int FLAG_CHECKABLE=1 << 2;
public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3;
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
this.parentID=parentID;
this.parentFragment=parentFragment;
@@ -51,6 +56,7 @@ public abstract class StatusDisplayItem{
public static BindableViewHolder<? extends StatusDisplayItem> createViewHolder(Type type, Activity activity, ViewGroup parent){
return switch(type){
case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent);
case HEADER_CHECKABLE -> new CheckableHeaderStatusDisplayItem.Holder(activity, parent);
case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent);
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
@@ -70,6 +76,15 @@ public abstract class StatusDisplayItem{
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter){
int flags=0;
if(inset)
flags|=FLAG_INSET;
if(!addFooter)
flags|=FLAG_NO_FOOTER;
return buildItems(fragment, status, accountID, parentObject, knownAccounts, flags);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, int flags){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
Status statusForContent=status.getContentStatus();
@@ -80,7 +95,10 @@ public abstract class StatusDisplayItem{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_reply_20px));
}
HeaderStatusDisplayItem header;
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
if((flags & FLAG_CHECKABLE)!=0)
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
else
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
ArrayList<StatusDisplayItem> contentItems;
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
@@ -99,7 +117,10 @@ public abstract class StatusDisplayItem{
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
if(!imageAttachments.isEmpty()){
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
contentItems.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
contentItems.add(mediaGrid);
}
for(Attachment att:statusForContent.mediaAttachments){
if(att.type==Attachment.Type.AUDIO){
@@ -112,12 +133,13 @@ public abstract class StatusDisplayItem{
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && TextUtils.isEmpty(statusForContent.spoilerText)){
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
}
if(addFooter){
if((flags & FLAG_NO_FOOTER)==0){
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment));
}
int i=1;
boolean inset=(flags & FLAG_INSET)!=0;
for(StatusDisplayItem item:items){
item.inset=inset;
item.index=i++;
@@ -156,7 +178,8 @@ public abstract class StatusDisplayItem{
EXTENDED_FOOTER,
MEDIA_GRID,
SPOILER,
SECTION_HEADER
SECTION_HEADER,
HEADER_CHECKABLE
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{

View File

@@ -35,6 +35,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.PopupMenu;
@@ -740,4 +741,13 @@ public class UiUtils{
ta.recycle();
return d;
}
public static WindowInsets applyBottomInsetToFixedView(View view, WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
view.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(40)) : 0);
return insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
}
return insets;
}
}

View File

@@ -228,6 +228,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
args.putParcelable("relationship", Parcels.wrap(relationship));
Nav.go(fragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(fragment.getActivity(), account.url);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.LinearLayout;
@@ -47,4 +48,11 @@ public class CheckableLinearLayout extends LinearLayout implements Checkable{
}
return drawableState;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(info);
info.setCheckable(true);
info.setChecked(checked);
}
}

View File

@@ -0,0 +1,58 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.RelativeLayout;
public class CheckableRelativeLayout extends RelativeLayout implements Checkable{
private boolean checked;
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public CheckableRelativeLayout(Context context){
this(context, null);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
public void setChecked(boolean checked){
this.checked=checked;
refreshDrawableState();
}
@Override
public boolean isChecked(){
return checked;
}
@Override
public void toggle(){
setChecked(!checked);
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(info);
info.setCheckable(true);
info.setChecked(checked);
}
}

View File

@@ -0,0 +1,83 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.Switch;
import java.lang.reflect.Field;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.V;
public class M3Switch extends Switch{
private boolean ignoreRequestLayout;
private DummyDrawable dummyDrawable=new DummyDrawable();
public M3Switch(Context context){
super(context);
}
public M3Switch(Context context, AttributeSet attrs){
super(context, attrs);
}
public M3Switch(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
ignoreRequestLayout=true;
Drawable prevThumbDrawable=getThumbDrawable();
setThumbDrawable(dummyDrawable);
ignoreRequestLayout=false;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ignoreRequestLayout=true;
setThumbDrawable(prevThumbDrawable);
ignoreRequestLayout=false;
try{
Field fld=Switch.class.getDeclaredField("mThumbWidth");
fld.setAccessible(true);
fld.set(this, V.dp(32));
}catch(Exception ignore){}
}
@Override
public void requestLayout(){
if(ignoreRequestLayout)
return;
super.requestLayout();
}
private static class DummyDrawable extends Drawable{
@Override
public void draw(@NonNull Canvas canvas){
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return 0;
}
@Override
public int getIntrinsicWidth(){
return V.dp(26);
}
}
}