mastodon-app-ufficiale-android/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java

1256 lines
45 KiB
Java

package org.joinmastodon.android.fragments;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.ImageSpanThatDoesNotBreakShitForNoGoodReason;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.CustomDrawingOrderLinearLayout;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
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;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel;
private ProgressBarButton actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private ProfileFeaturedFragment featuredFragment;
private AccountTimelineFragment timelineFragment;
private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
private View followersBtn, followingBtn;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
private LinearLayout countersLayout;
private View nameEditWrap, bioEditWrap;
private View tabsDivider;
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
private ImageButton qrCodeButton;
private ProgressBar innerProgress;
private View actions;
private Account account;
private String accountID;
private Relationship relationship;
private boolean isOwnProfile;
private ArrayList<AccountField> fields=new ArrayList<>();
private boolean isInEditMode, editDirty;
private Uri editNewAvatar, editNewCover;
private String profileAccountID;
private boolean refreshing;
private View fab;
private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
private ElevationOnScrollListener onScrollListener;
private Drawable tabsColorBackground;
private boolean tabBarIsAtTop;
private Animator tabBarColorAnim;
private MenuItem editSaveMenuItem;
private boolean savingEdits;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
accountID=getArguments().getString("account");
if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
loaded=true;
if(!isOwnProfile)
loadRelationship();
}else{
profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false))
loadData();
}
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setHasOptionsMenu(true);
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View content=inflater.inflate(R.layout.fragment_profile, container, false);
avatar=content.findViewById(R.id.avatar);
cover=content.findViewById(R.id.cover);
avatarBorder=content.findViewById(R.id.avatar_border);
name=content.findViewById(R.id.name);
username=content.findViewById(R.id.username);
usernameDomain=content.findViewById(R.id.username_domain);
bio=content.findViewById(R.id.bio);
followersCount=content.findViewById(R.id.followers_count);
followersLabel=content.findViewById(R.id.followers_label);
followersBtn=content.findViewById(R.id.followers_btn);
followingCount=content.findViewById(R.id.following_count);
followingLabel=content.findViewById(R.id.following_label);
followingBtn=content.findViewById(R.id.following_btn);
actionButton=content.findViewById(R.id.profile_action_btn);
pager=content.findViewById(R.id.pager);
scrollView=content.findViewById(R.id.scroller);
tabbar=content.findViewById(R.id.tabbar);
refreshLayout=content.findViewById(R.id.refresh_layout);
nameEdit=content.findViewById(R.id.name_edit);
bioEdit=content.findViewById(R.id.bio_edit);
nameEditWrap=content.findViewById(R.id.name_edit_wrap);
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
actionProgress=content.findViewById(R.id.action_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
countersLayout=content.findViewById(R.id.profile_counters);
tabsDivider=content.findViewById(R.id.tabs_divider);
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
scrollableContent=content.findViewById(R.id.scrollable_content);
qrCodeButton=content.findViewById(R.id.qr_code);
innerProgress=content.findViewById(R.id.profile_progress);
actions=content.findViewById(R.id.profile_actions);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-V.dp(48);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
};
tabViews=new FrameLayout[3];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.profile_featured;
case 1 -> R.id.profile_timeline;
case 2 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
sizeWrapper.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
tabViews[i]=tabView;
}
pager.setOffscreenPageLimit(4);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
scrollView.setScrollableChildSupplier(this::getScrollableRecyclerView);
sizeWrapper.addView(content);
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabbar.setTabTextSize(V.dp(14));
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, (tab, position)->tab.setText(switch(position){
case 0 -> R.string.profile_featured;
case 1 -> R.string.profile_timeline;
case 2 -> R.string.profile_about;
default -> throw new IllegalStateException();
}));
tabbar.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){}
@Override
public void onTabUnselected(TabLayout.Tab tab){}
@Override
public void onTabReselected(TabLayout.Tab tab){
if(getFragmentForPage(tab.getPosition()) instanceof ScrollableToTop stt)
stt.scrollToTop();
}
});
cover.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setEmpty();
}
});
actionButton.setOnClickListener(this::onActionButtonClick);
avatar.setOnClickListener(this::onAvatarClick);
cover.setOnClickListener(this::onCoverClick);
refreshLayout.setOnRefreshListener(this);
fab.setOnClickListener(this::onFabClick);
if(savedInstanceState!=null){
featuredFragment=(ProfileFeaturedFragment) getChildFragmentManager().getFragment(savedInstanceState, "featured");
timelineFragment=(AccountTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "timeline");
aboutFragment=(ProfileAboutFragment) getChildFragmentManager().getFragment(savedInstanceState, "about");
}
if(loaded){
bindHeaderView();
dataLoaded();
tabLayoutMediator.attach();
}else{
fab.setVisibility(View.GONE);
}
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnLongClickListener(v->{
if(account==null)
return true;
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
}
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
UiUtils.maybeShowTextCopiedToast(getActivity());
return true;
});
scrollableContent.setDrawingOrderCallback((count, pos)->{
// The header is the first child, draw it last to overlap everything for the photo viewer transition to look nice
if(pos==count-1)
return 0;
// Offset the order of other child views to compensate
return pos+1;
});
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
refreshLayout.setColorSchemeColors(colorPrimary);
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
usernameDomain.setOnClickListener(v->{
if(account==null)
return;
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
});
qrCodeButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
ProfileQrCodeFragment qf=new ProfileQrCodeFragment();
qf.setArguments(args);
qf.show(getChildFragmentManager(), "qrDialog");
});
return sizeWrapper;
}
@Override
protected void doLoadData(){
currentRequest=new GetAccountByID(profileAccountID)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(Account result){
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
dataLoaded();
if(!tabLayoutMediator.isAttached())
tabLayoutMediator.attach();
if(!isOwnProfile)
loadRelationship();
else
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(timelineFragment.loaded)
timelineFragment.onRefresh();
if(featuredFragment.loaded)
featuredFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
}
})
.exec(accountID);
}
@Override
public void onRefresh(){
if(refreshing)
return;
refreshing=true;
doLoadData();
}
@Override
public void dataLoaded(){
if(getActivity()==null)
return;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
args.putBoolean("__is_tab", true);
args.putBoolean("noAutoLoad", true);
if(featuredFragment==null){
featuredFragment=new ProfileFeaturedFragment();
featuredFragment.setArguments(args);
}
if(timelineFragment==null){
timelineFragment=AccountTimelineFragment.newInstance(accountID, account, true);
}
if(aboutFragment==null){
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
}
pager.getAdapter().notifyDataSetChanged();
pager.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
pager.getViewTreeObserver().removeOnPreDrawListener(this);
pager.setCurrentItem(1, false);
tabbar.selectTab(tabbar.getTabAt(1));
return true;
}
});
super.dataLoaded();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
updateToolbar();
// To avoid the callback triggering on first layout with position=0 before anything is instantiated
pager.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
pager.getViewTreeObserver().removeOnPreDrawListener(this);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageSelected(int position){
Fragment _page=getFragmentForPage(position);
if(_page instanceof BaseRecyclerFragment<?> page && page.isAdded()){
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
}
@Override
public void onPageScrollStateChanged(int state){
if(isInEditMode)
return;
refreshLayout.setEnabled(state!=ViewPager2.SCROLL_STATE_DRAGGING);
}
});
return true;
}
});
tabsColorBackground=((LayerDrawable)tabbar.getBackground()).findDrawableByLayerId(R.id.color_overlay);
onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar());
scrollView.setOnScrollChangeListener(this::onScrollChanged);
scrollView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
scrollView.getViewTreeObserver().removeOnPreDrawListener(this);
tabBarIsAtTop=!scrollView.canScrollVertically(1) && scrollView.getHeight()>0;
tabsColorBackground.setAlpha(tabBarIsAtTop ? 20 : 0);
tabbar.setTranslationZ(tabBarIsAtTop ? V.dp(3) : 0);
tabsDivider.setAlpha(tabBarIsAtTop ? 0 : 1);
return true;
}
});
if(!loaded)
bindHeaderViewForPreviewMaybe();
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
if(featuredFragment==null)
return;
if(featuredFragment.isAdded())
getChildFragmentManager().putFragment(outState, "featured", featuredFragment);
if(timelineFragment.isAdded())
getChildFragmentManager().putFragment(outState, "timeline", timelineFragment);
if(aboutFragment.isAdded())
getChildFragmentManager().putFragment(outState, "about", aboutFragment);
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(contentView!=null){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
int insetBottom=insets.getSystemWindowInsetBottom();
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+insetBottom;
applyChildWindowInsets();
insets=insets.inset(0, 0, 0, insetBottom);
}else{
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16);
}
}
super.onApplyWindowInsets(insets);
}
private void applyChildWindowInsets(){
if(timelineFragment!=null && timelineFragment.isAdded() && childInsets!=null){
timelineFragment.onApplyWindowInsets(childInsets);
featuredFragment.onApplyWindowInsets(childInsets);
}
}
private void bindHeaderViewForPreviewMaybe(){
if(loaded)
return;
String username=getArguments().getString("accountUsername");
String domain=getArguments().getString("accountDomain");
if(TextUtils.isEmpty(username) || TextUtils.isEmpty(domain))
return;
content.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
innerProgress.setVisibility(View.VISIBLE);
this.username.setText(username);
name.setText(username);
usernameDomain.setText(domain);
avatar.setImageResource(R.drawable.image_placeholder);
cover.setImageResource(R.drawable.image_placeholder);
actions.setVisibility(View.GONE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
tabsDivider.setVisibility(View.GONE);
}
private void bindHeaderView(){
if(innerProgress.getVisibility()==View.VISIBLE){
TransitionManager.beginDelayedTransition(contentView, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.excludeChildren(actions, true)
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
innerProgress.setVisibility(View.GONE);
countersLayout.setVisibility(View.VISIBLE);
actions.setVisibility(View.VISIBLE);
tabsDivider.setVisibility(View.VISIBLE);
}
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
if(account.locked){
ssb=new SpannableStringBuilder(account.username);
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
lock.setTint(username.getCurrentTextColor());
ssb.append(getString(R.string.manually_approves_followers), new ImageSpanThatDoesNotBreakShitForNoGoodReason(lock, ImageSpan.ALIGN_BOTTOM), 0);
username.setText(ssb);
}else{
username.setText(account.username);
}
String domain=account.getDomain();
if(TextUtils.isEmpty(domain))
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
}else{
bio.setVisibility(View.VISIBLE);
bio.setText(parsedBio);
}
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
UiUtils.loadCustomEmojiInTextView(name);
UiUtils.loadCustomEmojiInTextView(bio);
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
actionButton.setText(R.string.edit_profile);
TypedArray ta=actionButton.getContext().obtainStyledAttributes(R.style.Widget_Mastodon_M3_Button_Tonal, new int[]{android.R.attr.background});
actionButton.setBackground(ta.getDrawable(0));
ta.recycle();
ta=actionButton.getContext().obtainStyledAttributes(R.style.Widget_Mastodon_M3_Button_Tonal, new int[]{android.R.attr.textColor});
actionButton.setTextColor(ta.getColorStateList(0));
ta.recycle();
}else{
actionButton.setVisibility(View.GONE);
}
fields.clear();
AccountField joined=new AccountField();
joined.parsedName=joined.name=getString(R.string.profile_joined);
joined.parsedValue=joined.value=DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).format(LocalDateTime.ofInstant(account.createdAt, ZoneId.systemDefault()));
fields.add(joined);
for(AccountField field:account.fields){
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
ssb=new SpannableStringBuilder(field.name);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
field.parsedName=ssb;
field.nameEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
field.emojiRequests=new ArrayList<>(field.nameEmojis.length+field.valueEmojis.length);
for(CustomEmojiSpan span:field.nameEmojis){
field.emojiRequests.add(span.createImageLoaderRequest());
}
for(CustomEmojiSpan span:field.valueEmojis){
field.emojiRequests.add(span.createImageLoaderRequest());
}
fields.add(field);
}
if(aboutFragment!=null){
aboutFragment.setFields(fields);
}
}
private void updateToolbar(){
getToolbar().setOnClickListener(v->scrollToTop());
getToolbar().setNavigationContentDescription(R.string.back);
if(onScrollListener!=null){
onScrollListener.setViews(getToolbar());
}
}
private CharSequence makeRedString(CharSequence s){
int color=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
SpannableString ss=new SpannableString(s);
ss.setSpan(new ForegroundColorSpan(color), 0, ss.length(), 0);
return ss;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(isOwnProfile && isInEditMode){
editSaveMenuItem=menu.add(0, R.id.save, 0, R.string.save_changes);
editSaveMenuItem.setIcon(R.drawable.ic_save_24px);
editSaveMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
editSaveMenuItem.setVisible(!isActionButtonInView());
return;
}
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(R.string.share_user);
if(isOwnProfile)
return;
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(makeRedString(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername())));
menu.findItem(R.id.report).setTitle(makeRedString(getString(R.string.report_user, account.getDisplayUsername())));
if(relationship.following)
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user));
else
menu.findItem(R.id.hide_boosts).setVisible(false);
if(!account.isLocal())
menu.findItem(R.id.block_domain).setTitle(makeRedString(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain())));
else
menu.findItem(R.id.block_domain).setVisible(false);
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
if(relationship.following){
MenuItem notifications=menu.findItem(R.id.notifications);
notifications.setVisible(true);
notifications.setIcon(relationship.notifying ? R.drawable.ic_notifications_fill1_24px : R.drawable.ic_notifications_24px);
notifications.setTitle(getString(relationship.notifying ? R.string.disable_new_post_notifications : R.string.enable_new_post_notifications, account.getDisplayUsername()));
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()){
menu.setGroupDividerEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
UiUtils.openSystemShareSheet(getActivity(), account);
}else if(id==R.id.mute){
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));
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);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account, relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
updateRelationship();
}, this::updateRelationship);
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
updateRelationship(result);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.bookmarks){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), BookmarkedStatusListFragment.class, args);
}else if(id==R.id.favorites){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
}else if(id==R.id.save){
if(isInEditMode)
saveAndExitEditMode();
}else if(id==R.id.add_to_list){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
}else if(id==R.id.notifications){
new SetAccountFollowed(account.id, true, relationship.showingReblogs, !relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
updateRelationship(result);
new Snackbar.Builder(getActivity())
.setText(result.notifying ? R.string.new_post_notifications_enabled : R.string.new_post_notifications_disabled)
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
return true;
}
private void loadRelationship(){
new GetAccountRelationships(Collections.singletonList(account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
if(!result.isEmpty()){
relationship=result.get(0);
updateRelationship();
}
}
@Override
public void onError(ErrorResponse error){
}
})
.exec(accountID);
}
private void updateRelationship(){
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
if(scrollY>cover.getHeight()){
cover.setTranslationY(scrollY-(cover.getHeight()));
cover.setTranslationZ(V.dp(10));
cover.setTransform(cover.getHeight()/2f);
}else{
cover.setTranslationY(0f);
cover.setTranslationZ(0f);
cover.setTransform(scrollY/2f);
}
cover.invalidate();
if(currentPhotoViewer!=null){
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
}
onScrollListener.onScrollChange(v, scrollX, scrollY, oldScrollX, oldScrollY);
boolean newTabBarIsAtTop=!scrollView.canScrollVertically(1);
if(newTabBarIsAtTop!=tabBarIsAtTop){
tabBarIsAtTop=newTabBarIsAtTop;
if(tabBarIsAtTop){
// ScrollView would sometimes leave 1 pixel unscrolled, force it into the correct scrollY
int maxY=scrollView.getChildAt(0).getHeight()-scrollView.getHeight();
if(scrollView.getScrollY()!=maxY)
scrollView.scrollTo(0, maxY);
}
if(tabBarColorAnim!=null)
tabBarColorAnim.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(tabsColorBackground, "alpha", tabBarIsAtTop ? 20 : 0),
ObjectAnimator.ofFloat(tabbar, View.TRANSLATION_Z, tabBarIsAtTop ? V.dp(3) : 0),
ObjectAnimator.ofFloat(getToolbar(), View.TRANSLATION_Z, tabBarIsAtTop ? 0 : V.dp(3)),
ObjectAnimator.ofFloat(tabsDivider, View.ALPHA, tabBarIsAtTop ? 0 : 1)
);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
tabBarColorAnim=null;
}
});
tabBarColorAnim=set;
set.start();
}
if(isInEditMode && editSaveMenuItem!=null){
boolean buttonInView=isActionButtonInView();
if(buttonInView==editSaveMenuItem.isVisible()){
editSaveMenuItem.setVisible(!buttonInView);
}
}
}
private Fragment getFragmentForPage(int page){
return switch(page){
case 0 -> featuredFragment;
case 1 -> timelineFragment;
case 2 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
private RecyclerView getScrollableRecyclerView(){
return getFragmentForPage(pager.getCurrentItem()).getView().findViewById(R.id.list);
}
private void onActionButtonClick(View v){
if(isOwnProfile){
if(!isInEditMode)
loadAccountInfoAndEnterEditMode();
else
saveAndExitEditMode();
}else{
UiUtils.performAccountAction(getActivity(), account, accountID, relationship, actionButton, this::setActionProgressVisible, this::updateRelationship);
}
}
private void setActionProgressVisible(boolean visible){
actionButton.setTextVisible(!visible);
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
if(visible)
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
actionButton.setClickable(!visible);
}
private void loadAccountInfoAndEnterEditMode(){
if(editModeLoading)
return;
editModeLoading=true;
setActionProgressVisible(true);
new GetOwnAccount()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
editModeLoading=false;
if(getActivity()==null)
return;
enterEditMode(result);
setActionProgressVisible(false);
}
@Override
public void onError(ErrorResponse error){
editModeLoading=false;
if(getActivity()==null)
return;
error.showToast(getActivity());
setActionProgressVisible(false);
}
})
.exec(accountID);
}
private void enterEditMode(Account account){
if(isInEditMode)
throw new IllegalStateException();
isInEditMode=true;
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.save_changes);
pager.setCurrentItem(2);
for(int i=0;i<3;i++){
tabbar.getTabAt(i).view.setEnabled(false);
}
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
avatar.setForeground(overlay);
Toolbar toolbar=getToolbar();
Drawable close=getToolbarContext().getDrawable(R.drawable.ic_baseline_close_24).mutate();
close.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
toolbar.setNavigationIcon(close);
toolbar.setNavigationContentDescription(R.string.discard);
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
Runnable updater=new Runnable(){
@Override
public void run(){
// setPadding() calls nullLayouts() internally, forcing the text layout to update
actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0);
actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0);
actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY);
actionButton.postOnAnimation(this);
}
};
actionButton.postOnAnimation(updater);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.addListener(new Transition.TransitionListener(){
@Override
public void onTransitionStart(Transition transition){}
@Override
public void onTransitionEnd(Transition transition){
actionButton.removeCallbacks(updater);
}
@Override
public void onTransitionCancel(Transition transition){}
@Override
public void onTransitionPause(Transition transition){}
@Override
public void onTransitionResume(Transition transition){}
})
);
name.setVisibility(View.INVISIBLE);
username.setVisibility(View.INVISIBLE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
qrCodeButton.setVisibility(View.GONE);
usernameDomain.setVisibility(View.INVISIBLE);
nameEditWrap.setVisibility(View.VISIBLE);
nameEdit.setText(account.displayName);
bioEditWrap.setVisibility(View.VISIBLE);
bioEdit.setText(account.source.note);
aboutFragment.enterEditMode(account.source.fields);
refreshLayout.setEnabled(false);
editDirty=false;
V.setVisibilityAnimated(fab, View.GONE);
}
private void exitEditMode(){
if(!isInEditMode)
throw new IllegalStateException();
isInEditMode=false;
invalidateOptionsMenu();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
avatar.setForeground(null);
Toolbar toolbar=getToolbar();
if(canGoBack()){
Drawable back=getToolbarContext().getDrawable(me.grishka.appkit.R.drawable.ic_arrow_back).mutate();
back.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
toolbar.setNavigationIcon(back);
toolbar.setNavigationContentDescription(0);
}else{
toolbar.setNavigationIcon(null);
}
editSaveMenuItem=null;
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
Runnable updater=new Runnable(){
@Override
public void run(){
// setPadding() calls nullLayouts() internally, forcing the text layout to update
actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0);
actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0);
actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY);
actionButton.postOnAnimation(this);
}
};
actionButton.postOnAnimation(updater);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.addListener(new Transition.TransitionListener(){
@Override
public void onTransitionStart(Transition transition){}
@Override
public void onTransitionEnd(Transition transition){
actionButton.removeCallbacks(updater);
}
@Override
public void onTransitionCancel(Transition transition){}
@Override
public void onTransitionPause(Transition transition){}
@Override
public void onTransitionResume(Transition transition){}
})
);
nameEditWrap.setVisibility(View.GONE);
bioEditWrap.setVisibility(View.GONE);
name.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
bio.setVisibility(View.VISIBLE);
countersLayout.setVisibility(View.VISIBLE);
refreshLayout.setEnabled(true);
usernameDomain.setVisibility(View.VISIBLE);
qrCodeButton.setVisibility(View.VISIBLE);
bindHeaderView();
V.setVisibilityAnimated(fab, View.VISIBLE);
}
private void saveAndExitEditMode(){
if(!isInEditMode)
throw new IllegalStateException();
setActionProgressVisible(true);
savingEdits=true;
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, aboutFragment.getFields())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
savingEdits=false;
account=result;
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
exitEditMode();
setActionProgressVisible(false);
}
@Override
public void onError(ErrorResponse error){
savingEdits=false;
error.showToast(getActivity());
setActionProgressVisible(false);
}
})
.exec(accountID);
}
private void confirmToggleMuted(){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}
private void confirmToggleBlocked(){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}
private void updateRelationship(Relationship r){
relationship=r;
updateRelationship();
}
@Override
public boolean onBackPressed(){
if(isInEditMode){
if(savingEdits)
return true;
if(editDirty || aboutFragment.isEditDirty()){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_changes)
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
.setNegativeButton(R.string.cancel, null)
.show();
}else{
exitEditMode();
}
return true;
}
return false;
}
private List<Attachment> createFakeAttachments(String url, Drawable drawable){
Attachment att=new Attachment();
att.type=Attachment.Type.IMAGE;
att.url=url;
att.meta=new Attachment.Metadata();
att.meta.width=drawable.getIntrinsicWidth();
att.meta.height=drawable.getIntrinsicHeight();
return Collections.singletonList(att);
}
private void onAvatarClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(AVATAR_RESULT);
}else{
Drawable ava=avatar.getDrawable();
if(ava==null)
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
private void onCoverClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(COVER_RESULT);
}else{
Drawable drawable=cover.getDrawable();
if(drawable==null || drawable instanceof ColorDrawable)
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
}
}
private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
if(!AccountSessionManager.getInstance().isSelf(accountID, account)){
args.putString("prefilledText", '@'+account.acct+' ');
}
Nav.go(getActivity(), ComposeFragment.class, args);
}
private void startImagePicker(int requestCode){
Intent intent=UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1);
startActivityForResult(intent, requestCode);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode==Activity.RESULT_OK){
if(requestCode==AVATAR_RESULT){
editNewAvatar=data.getData();
ViewImageLoader.loadWithoutAnimation(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
editDirty=true;
}else if(requestCode==COVER_RESULT){
editNewCover=data.getData();
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
editDirty=true;
}
}
}
@Override
public void scrollToTop(){
getScrollableRecyclerView().scrollToPosition(0);
scrollView.smoothScrollTo(0, 0);
}
private void onFollowersOrFollowingClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Class<? extends Fragment> cls;
if(v.getId()==R.id.followers_btn)
cls=FollowerListFragment.class;
else if(v.getId()==R.id.following_btn)
cls=FollowingListFragment.class;
else
return;
Nav.go(getActivity(), cls, args);
}
private boolean isActionButtonInView(){
return actionButton.getVisibility()==View.VISIBLE && actionButtonWrap.getTop()+actionButtonWrap.getHeight()>scrollView.getScrollY();
}
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=new FrameLayout(parent.getContext());
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
Fragment fragment=getFragmentForPage(position);
FrameLayout fragmentView=tabViews[position];
fragmentView.setVisibility(View.VISIBLE);
if(fragmentView.getParent() instanceof ViewGroup parent)
parent.removeView(fragmentView);
((FrameLayout)holder.itemView).addView(fragmentView);
if(!fragment.isAdded()){
getChildFragmentManager().beginTransaction().add(fragmentView.getId(), fragment).commit();
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
getChildFragmentManager().executePendingTransactions();
if(fragment.isAdded()){
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
applyChildWindowInsets();
}
return true;
}
});
}
}
@Override
public int getItemCount(){
return loaded ? 3 : 0;
}
@Override
public int getItemViewType(int position){
return position;
}
}
}