Improve follow recommendations screen (AND-101)

This commit is contained in:
Grishka 2023-11-29 02:09:59 +03:00
parent e797d8a1c2
commit a2ea8e76fb
10 changed files with 218 additions and 36 deletions

View File

@ -38,6 +38,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
protected int itemLayoutRes=R.layout.item_account_list;
public BaseAccountListFragment(){
super(40);
@ -151,7 +152,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes);
onConfigureViewHolder(holder);
return holder;
}

View File

@ -4,6 +4,7 @@ import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
@ -12,35 +13,38 @@ import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
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.views.FragmentRootLinearLayout;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
private String accountID;
private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0;
public OnboardingFollowSuggestionsFragment(){
super(R.layout.fragment_onboarding_follow_suggestions, 40);
itemLayoutRes=R.layout.item_account_list_onboarding;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.popular_on_mastodon);
setTitle(R.string.onboarding_recommendations_title);
accountID=getArguments().getString("account");
loadData();
}
@ -49,7 +53,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar);
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
@ -58,9 +61,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
getToolbar().setContentInsetsRelative(V.dp(56), 0);
}
@Override
@ -69,7 +70,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
}
})
.exec(accountID);
@ -80,6 +81,19 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
TextView introText=new TextView(getActivity());
introText.setTextAppearance(R.style.m3_body_large);
introText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
introText.setPaddingRelative(V.dp(56), 0, V.dp(24), V.dp(8));
introText.setText(R.string.onboarding_recommendations_intro);
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(introText));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void onFollowAllClick(View v){
if(!loaded || relationships.isEmpty())
return;
@ -155,5 +169,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
holder.avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
}
}

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.model.viewmodel;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import org.joinmastodon.android.GlobalUserPreferences;
@ -7,6 +8,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.LinkSpan;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import java.util.Collections;
@ -43,4 +45,13 @@ public class AccountViewModel{
}
this.verifiedLink=verifiedLink;
}
public AccountViewModel stripLinksFromBio(){
if(parsedBio instanceof Spannable spannable){
for(LinkSpan span:spannable.getSpans(0, spannable.length(), LinkSpan.class)){
spannable.removeSpan(span);
}
}
return this;
}
}

View File

@ -44,6 +44,7 @@ import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import androidx.annotation.LayoutRes;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@ -53,7 +54,7 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class AccountViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name, username, followers, verifiedLink, bio;
private final ImageView avatar;
public final ImageView avatar;
private final ProgressBarButton button;
private final PopupMenu contextMenu;
private final View menuAnchor;
@ -75,7 +76,11 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
private boolean checked;
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment.getActivity(), R.layout.item_account_list, list);
this(fragment, list, relationships, R.layout.item_account_list);
}
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships, @LayoutRes int layout){
super(fragment.getActivity(), layout, list);
this.fragment=fragment;
this.accountID=Objects.requireNonNull(fragment.getArguments().getString("account"));
this.relationships=relationships;
@ -111,24 +116,28 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
public void onBind(AccountViewModel item){
name.setText(item.parsedName);
username.setText("@"+item.account.acct);
String followersStr=fragment.getResources().getQuantityString(R.plurals.x_followers, item.account.followersCount>1000 ? 999 : (int)item.account.followersCount);
String followersNum=UiUtils.abbreviateNumber(item.account.followersCount);
int index=followersStr.indexOf("%,d");
followersStr=followersStr.replace("%,d", followersNum);
SpannableStringBuilder followersFormatted=new SpannableStringBuilder(followersStr);
if(index!=-1){
followersFormatted.setSpan(mediumSpan, index, index+followersNum.length(), 0);
if(followers!=null){
String followersStr=fragment.getResources().getQuantityString(R.plurals.x_followers, item.account.followersCount>1000 ? 999 : (int)item.account.followersCount);
String followersNum=UiUtils.abbreviateNumber(item.account.followersCount);
int index=followersStr.indexOf("%,d");
followersStr=followersStr.replace("%,d", followersNum);
SpannableStringBuilder followersFormatted=new SpannableStringBuilder(followersStr);
if(index!=-1){
followersFormatted.setSpan(mediumSpan, index, index+followersNum.length(), 0);
}
followers.setText(followersFormatted);
}
if(verifiedLink!=null){
boolean hasVerifiedLink=item.verifiedLink!=null;
if(!hasVerifiedLink)
verifiedLink.setText(R.string.no_verified_link);
else
verifiedLink.setText(item.verifiedLink);
verifiedLink.setCompoundDrawablesRelativeWithIntrinsicBounds(hasVerifiedLink ? R.drawable.ic_check_small_16px : R.drawable.ic_help_16px, 0, 0, 0);
int tintColor=UiUtils.getThemeColor(fragment.getActivity(), hasVerifiedLink ? R.attr.colorM3Primary : R.attr.colorM3Secondary);
verifiedLink.setTextColor(tintColor);
verifiedLink.setCompoundDrawableTintList(ColorStateList.valueOf(tintColor));
}
followers.setText(followersFormatted);
boolean hasVerifiedLink=item.verifiedLink!=null;
if(!hasVerifiedLink)
verifiedLink.setText(R.string.no_verified_link);
else
verifiedLink.setText(item.verifiedLink);
verifiedLink.setCompoundDrawablesRelativeWithIntrinsicBounds(hasVerifiedLink ? R.drawable.ic_check_small_16px : R.drawable.ic_help_16px, 0, 0, 0);
int tintColor=UiUtils.getThemeColor(fragment.getActivity(), hasVerifiedLink ? R.attr.colorM3Primary : R.attr.colorM3Secondary);
verifiedLink.setTextColor(tintColor);
verifiedLink.setCompoundDrawableTintList(ColorStateList.valueOf(tintColor));
bindRelationship();
if(showBio){
bio.setText(item.parsedBio);
@ -338,7 +347,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
Menu menu=contextMenu.getMenu();
Account account=item.account;
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
menu.findItem(R.id.share).setTitle(R.string.share_user);
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/m3_primary_overlay" xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:tint="@color/m3_primary_alpha5" android:tintMode="src_over">
<solid android:color="?colorM3Surface"/>
<corners android:radius="20dp"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="?colorM3OutlineVariant"/>
<corners android:radius="7dp"/>
</shape>

View File

@ -37,16 +37,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:background="@drawable/bg_onboarding_panel">
android:background="@drawable/bg_m3_surface1">
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="16dp"
style="@style/Widget.Mastodon.M3.Button.Tonal"
style="@style/Widget.Mastodon.M3.Button.Elevated"
android:text="@string/follow_all"/>
<Button
@ -55,10 +55,10 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="16dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/skip"/>
android:text="@string/next"/>
</LinearLayout>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.CheckableRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/avatar"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
android:foreground="@drawable/fg_onboarding_ava"
android:padding="1dp"
tools:src="#0f0"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/accessory"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurface"
tools:text="User"/>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_below="@id/name"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/accessory"
android:layout_marginTop="-2dp"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical|start"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3Secondary"
android:textAlignment="viewStart"
tools:text="\@user@server"/>
<FrameLayout
android:id="@+id/accessory"
android:layout_width="wrap_content"
android:layout_height="38dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginStart="8dp"
android:duplicateParentState="true">
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:paddingHorizontal="10dp"
android:minWidth="96dp"
android:maxWidth="150dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
tools:text="Follow back"/>
<ProgressBar
android:id="@+id/action_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
style="?android:progressBarStyleSmall"
android:elevation="10dp"
android:outlineProvider="none"
android:indeterminateTint="?colorM3OnPrimary"
android:visibility="gone"/>
<View
android:id="@+id/checkbox"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginHorizontal="4dp"
android:layout_marginTop="2dp"
android:duplicateParentState="true"
android:visibility="gone"/>
<ImageButton
android:id="@+id/options_btn"
android:layout_width="40dp"
android:layout_height="36dp"
android:layout_gravity="top"
android:background="?android:actionBarItemBackground"
android:tint="?colorM3OnSurfaceVariant"
android:contentDescription="@string/more_options"
android:src="@drawable/ic_more_vert_24px"
android:visibility="gone"/>
</FrameLayout>
<TextView
android:id="@+id/bio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/avatar"
android:layout_below="@id/username"
android:layout_marginTop="2dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurface"
android:maxLines="2"
android:paddingVertical="2dp"
tools:text="bla bla bla bla bla bla"/>
<View
android:id="@+id/menu_anchor"
android:layout_width="1px"
android:layout_height="1px"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="-16dp"/>
</org.joinmastodon.android.ui.views.CheckableRelativeLayout>

View File

@ -350,7 +350,6 @@
<string name="profile_add_row">Add row</string>
<string name="profile_setup">Profile setup</string>
<string name="profile_setup_subtitle">You can always complete this later in the Profile tab.</string>
<string name="popular_on_mastodon">Popular on Mastodon</string>
<string name="follow_all">Follow all</string>
<string name="server_rules_disagree">Disagree</string>
<string name="privacy_policy_explanation">TL;DR: We dont collect or process anything.</string>
@ -664,4 +663,6 @@
<string name="discoverability">Discoverability</string>
<string name="discoverability_help">When you opt into discoverability on Mastodon, your posts may appear in search results and trending.\n\nYour profile may be suggested to people with similar interests to you.\n\nOpting out does not hide your profile if someone searches for you by name.</string>
<string name="app_version_copied">Version number copied to clipboard</string>
<string name="onboarding_recommendations_intro">You curate your own home feed.The more people you follow, the more active and interesting it will be.</string>
<string name="onboarding_recommendations_title">Personalize your home feed</string>
</resources>

View File

@ -289,6 +289,14 @@
<item name="android:textColor">@color/button_text_m3_tonal_error</item>
</style>
<style name="Widget.Mastodon.M3.Button.Elevated">
<item name="android:background">@drawable/bg_button_m3_elevated</item>
<item name="android:textColor">@color/button_text_m3_text</item>
<item name="android:paddingLeft">24dp</item>
<item name="android:paddingRight">24dp</item>
<item name="android:elevation">1dp</item>
</style>
<style name="Widget.Mastodon.M3.Button.Outlined">
<item name="android:background">@drawable/bg_button_m3_outlined</item>
<item name="android:textColor">@color/button_text_m3_text</item>