From 56a2510564b887e32e7e85ef53c4230c0863b3cd Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 6 Nov 2024 11:56:04 +0300 Subject: [PATCH] Show mutual followers in profile (AND-187) --- .../accounts/GetAccountFamiliarFollowers.java | 18 +++++ .../android/fragments/ProfileFragment.java | 78 +++++++++++++++++++ .../FamiliarFollowerListFragment.java | 48 ++++++++++++ .../android/model/FamiliarFollowers.java | 20 +++++ .../ui/views/FamiliarFollowersImageView.java | 59 ++++++++++++++ .../src/main/res/layout/fragment_profile.xml | 56 +++++++++++++ mastodon/src/main/res/values/strings.xml | 10 +++ 7 files changed, 289 insertions(+) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountFamiliarFollowers.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FamiliarFollowerListFragment.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/model/FamiliarFollowers.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/FamiliarFollowersImageView.java diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountFamiliarFollowers.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountFamiliarFollowers.java new file mode 100644 index 000000000..2d96d7095 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountFamiliarFollowers.java @@ -0,0 +1,18 @@ +package org.joinmastodon.android.api.requests.accounts; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.FamiliarFollowers; + +import java.util.Collection; +import java.util.List; + +public class GetAccountFamiliarFollowers extends MastodonAPIRequest>{ + public GetAccountFamiliarFollowers(Collection ids){ + super(HttpMethod.GET, "/accounts/familiar_followers", new TypeToken<>(){}); + for(String id:ids){ + addQueryParameter("id[]", id); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 6be4565d2..7e6b49464 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -51,18 +51,22 @@ 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.GetAccountFamiliarFollowers; 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.FamiliarFollowerListFragment; 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.FamiliarFollowers; import org.joinmastodon.android.model.Relationship; +import org.joinmastodon.android.model.viewmodel.AccountViewModel; import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.SimpleViewHolder; @@ -92,6 +96,8 @@ import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; @@ -139,12 +145,16 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop, private ImageButton qrCodeButton; private ProgressBar innerProgress; private View actions; + private View familiarFollowersRow; + private ImageView[] familiarFollowersAvatars; + private TextView familiarFollowersLabel; private Account account; private String accountID; private Relationship relationship; private boolean isOwnProfile; private ArrayList fields=new ArrayList<>(); + private List familiarFollowers=List.of(); private boolean isInEditMode, editDirty; private Uri editNewAvatar, editNewCover; @@ -225,6 +235,13 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop, qrCodeButton=content.findViewById(R.id.qr_code); innerProgress=content.findViewById(R.id.profile_progress); actions=content.findViewById(R.id.profile_actions); + familiarFollowersRow=content.findViewById(R.id.familiar_followers); + familiarFollowersAvatars=new ImageView[]{ + content.findViewById(R.id.familiar_followers_ava1), + content.findViewById(R.id.familiar_followers_ava2), + content.findViewById(R.id.familiar_followers_ava3), + }; + familiarFollowersLabel=content.findViewById(R.id.familiar_followers_label); avatar.setOutlineProvider(OutlineProviders.roundedRect(24)); avatar.setClipToOutline(true); @@ -293,6 +310,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop, cover.setOnClickListener(this::onCoverClick); refreshLayout.setOnRefreshListener(this); fab.setOnClickListener(this::onFabClick); + familiarFollowersRow.setOnClickListener(this::onFamiliarFollowersClick); if(savedInstanceState!=null){ featuredFragment=(ProfileFeaturedFragment) getChildFragmentManager().getFragment(savedInstanceState, "featured"); @@ -352,6 +370,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop, qf.setArguments(args); qf.show(getChildFragmentManager(), "qrDialog"); }); + familiarFollowersRow.setVisibility(View.GONE); return sizeWrapper; } @@ -790,6 +809,25 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop, } }) .exec(accountID); + new GetAccountFamiliarFollowers(Set.of(account.id)) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + for(FamiliarFollowers ff:result){ + if(ff.id.equals(account.id)){ + familiarFollowers=ff.accounts; + updateFamiliarFollowers(); + break; + } + } + } + + @Override + public void onError(ErrorResponse error){ + + } + }) + .exec(accountID); } private void updateRelationship(){ @@ -800,6 +838,38 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop, followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE); } + private void updateFamiliarFollowers(){ + if(!familiarFollowers.isEmpty()){ + familiarFollowersRow.setVisibility(View.VISIBLE); + List followers=familiarFollowers.stream().limit(3).map(a->new AccountViewModel(a, accountID, false, getActivity())).collect(Collectors.toList()); + String template=switch(familiarFollowers.size()){ + case 1 -> getString(R.string.familiar_followers_one, "{first}"); + case 2 -> getString(R.string.familiar_followers_two, "{first}", "{second}"); + default -> getResources().getQuantityString(R.plurals.familiar_followers_many, familiarFollowers.size()-2, "{first}", "{second}", familiarFollowers.size()-2); + }; + SpannableStringBuilder ssb=new SpannableStringBuilder(template); + if(familiarFollowers.size()>1){ + int index=template.indexOf("{second}"); + ssb.replace(index, index+8, followers.get(1).parsedName); + template=template.replace("{second}", "#".repeat(followers.get(1).parsedName.length())); + } + int index=template.indexOf("{first}"); + ssb.replace(index, index+7, followers.get(0).parsedName); + familiarFollowersLabel.setText(ssb); + UiUtils.loadCustomEmojiInTextView(familiarFollowersLabel); + if(familiarFollowers.size()<3) + familiarFollowersAvatars[2].setVisibility(View.GONE); + if(familiarFollowers.size()<2) + familiarFollowersAvatars[1].setVisibility(View.GONE); + + int i=0; + for(AccountViewModel avm:followers){ + ViewImageLoader.loadWithoutAnimation(familiarFollowersAvatars[i], getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), avm.avaRequest); + i++; + } + } + } + private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){ if(scrollY>cover.getHeight()){ cover.setTranslationY(scrollY-(cover.getHeight())); @@ -1168,6 +1238,14 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop, Nav.go(getActivity(), ComposeFragment.class, args); } + private void onFamiliarFollowersClick(View v){ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("targetAccount", Parcels.wrap(account)); + args.putInt("count", familiarFollowers.size()); + Nav.go(getActivity(), FamiliarFollowerListFragment.class, args); + } + private void startImagePicker(int requestCode){ Intent intent=UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1); startActivityForResult(intent, requestCode); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FamiliarFollowerListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FamiliarFollowerListFragment.java new file mode 100644 index 000000000..c8489d492 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FamiliarFollowerListFragment.java @@ -0,0 +1,48 @@ +package org.joinmastodon.android.fragments.account_list; + +import android.os.Bundle; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.accounts.GetAccountFamiliarFollowers; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.FamiliarFollowers; +import org.joinmastodon.android.model.viewmodel.AccountViewModel; +import org.parceler.Parcels; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import me.grishka.appkit.api.SimpleCallback; + +public class FamiliarFollowerListFragment extends BaseAccountListFragment{ + protected Account account; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + account=Parcels.unwrap(getArguments().getParcelable("targetAccount")); + setTitle("@"+account.acct); + int count=getArguments().getInt("count"); + setSubtitle(getResources().getQuantityString(R.plurals.x_followers_you_know, count, count)); + } + + @Override + protected void doLoadData(int offset, int count){ + currentRequest=new GetAccountFamiliarFollowers(Set.of(account.id)) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(List result){ + onDataLoaded(result.get(0).accounts.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), false); + } + }) + .exec(accountID); + } + + @Override + public void onResume(){ + super.onResume(); + if(!loaded && !dataLoading) + loadData(); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/FamiliarFollowers.java b/mastodon/src/main/java/org/joinmastodon/android/model/FamiliarFollowers.java new file mode 100644 index 000000000..e7d23e4a9 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/FamiliarFollowers.java @@ -0,0 +1,20 @@ +package org.joinmastodon.android.model; + +import org.joinmastodon.android.api.AllFieldsAreRequired; +import org.joinmastodon.android.api.ObjectValidationException; + +import java.util.List; + +@AllFieldsAreRequired +public class FamiliarFollowers extends BaseModel{ + public String id; + public List accounts; + + @Override + public void postprocess() throws ObjectValidationException{ + super.postprocess(); + for(Account acc:accounts){ + acc.postprocess(); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FamiliarFollowersImageView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FamiliarFollowersImageView.java new file mode 100644 index 000000000..c731ceb35 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FamiliarFollowersImageView.java @@ -0,0 +1,59 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.ImageView; + +import org.joinmastodon.android.ui.utils.UiUtils; + +import me.grishka.appkit.utils.CustomViewHelper; + +public class FamiliarFollowersImageView extends ImageView implements CustomViewHelper{ + private Paint clearPaint=new Paint(Paint.ANTI_ALIAS_FLAG); + private Path path=new Path(), rectPath=new Path(); + + public FamiliarFollowersImageView(Context context){ + this(context, null); + } + + public FamiliarFollowersImageView(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public FamiliarFollowersImageView(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + UiUtils.setAllPaddings(this, 2); + setOutlineProvider(new ViewOutlineProvider(){ + @Override + public void getOutline(View view, Outline outline){ + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), getResources().getDisplayMetrics().density*8.5f); + } + }); + setClipToOutline(true); + clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + + @Override + protected void onDraw(Canvas canvas){ + float offset=dp(2); + float radius=dp(6); + rectPath.rewind(); + rectPath.addRoundRect(offset, offset, getWidth()-offset, getHeight()-offset, radius, radius, Path.Direction.CW); + canvas.save(); + canvas.clipPath(rectPath); // Unless I do this, the corner pixels still end up dirty + super.onDraw(canvas); + canvas.restore(); + path.rewind(); + path.addRect(0, 0, getWidth(), getHeight(), Path.Direction.CW); + path.op(rectPath, Path.Op.DIFFERENCE); + canvas.drawPath(path, clearPaint); + } +} diff --git a/mastodon/src/main/res/layout/fragment_profile.xml b/mastodon/src/main/res/layout/fragment_profile.xml index 6d49a756a..1c31c5f68 100644 --- a/mastodon/src/main/res/layout/fragment_profile.xml +++ b/mastodon/src/main/res/layout/fragment_profile.xml @@ -239,6 +239,62 @@ + + + + + + + + + + + + + %1$s and %2$,d other followed you %1$s and %2$,d others followed you + Followed by %s + Followed by %s and %s + + Followed by %1$s, %2$s, and %3$,d other + Followed by %1$s, %2$s, and %3$,d others + + + %,d follower you know + %,d followers you know + \ No newline at end of file