Show mutual followers in profile (AND-187)

This commit is contained in:
Grishka
2024-11-06 11:56:04 +03:00
parent ddaab49976
commit 56a2510564
7 changed files with 289 additions and 0 deletions

View File

@@ -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<List<FamiliarFollowers>>{
public GetAccountFamiliarFollowers(Collection<String> ids){
super(HttpMethod.GET, "/accounts/familiar_followers", new TypeToken<>(){});
for(String id:ids){
addQueryParameter("id[]", id);
}
}
}

View File

@@ -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<AccountField> fields=new ArrayList<>();
private List<Account> 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<FamiliarFollowers> 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<AccountViewModel> 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);

View File

@@ -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<FamiliarFollowers> 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();
}
}

View File

@@ -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<Account> accounts;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
for(Account acc:accounts){
acc.postprocess();
}
}
}

View File

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

View File

@@ -239,6 +239,62 @@
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/familiar_followers"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginBottom="12dp"
android:paddingVertical="4dp"
android:paddingHorizontal="16dp"
android:clipToPadding="false"
android:orientation="horizontal"
android:background="?android:selectableItemBackground">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginVertical="-4dp"
android:paddingTop="4dp"
android:clipToPadding="false"
android:layerType="hardware">
<org.joinmastodon.android.ui.views.FamiliarFollowersImageView
android:id="@+id/familiar_followers_ava1"
android:layout_width="30dp"
android:layout_height="30dp"
android:rotation="-4"
tools:src="#f00"/>
<org.joinmastodon.android.ui.views.FamiliarFollowersImageView
android:id="@+id/familiar_followers_ava2"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="-10dp"
android:rotation="2"
tools:src="#0f0"/>
<org.joinmastodon.android.ui.views.FamiliarFollowersImageView
android:id="@+id/familiar_followers_ava3"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="-10dp"
android:rotation="-2"
tools:src="#00f"/>
</LinearLayout>
<TextView
android:id="@+id/familiar_followers_label"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:gravity="start|center_vertical"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:maxLines="2"
android:ellipsize="end"
tools:text="Followed by blah and blah"/>
</LinearLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/name_edit_wrap"

View File

@@ -827,4 +827,14 @@
<item quantity="one">%1$s and %2$,d other followed you</item>
<item quantity="other">%1$s and %2$,d others followed you</item>
</plurals>
<string name="familiar_followers_one">Followed by %s</string>
<string name="familiar_followers_two">Followed by %s and %s</string>
<plurals name="familiar_followers_many">
<item quantity="one">Followed by %1$s, %2$s, and %3$,d other</item>
<item quantity="other">Followed by %1$s, %2$s, and %3$,d others</item>
</plurals>
<plurals name="x_followers_you_know">
<item quantity="one">%,d follower you know</item>
<item quantity="other">%,d followers you know</item>
</plurals>
</resources>