Pull user row into a separate view holder & update its design

This commit is contained in:
Grishka 2023-05-11 03:45:23 +03:00
parent cfabe47e10
commit e253d8f4f3
11 changed files with 378 additions and 241 deletions

View File

@ -1,39 +1,21 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment; import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -44,19 +26,15 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<BaseAccountListFragment.AccountItem>{ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<AccountViewModel>{
protected HashMap<String, Relationship> relationships=new HashMap<>(); protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID; protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>(); protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@ -72,7 +50,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<B
} }
@Override @Override
protected void onDataLoaded(List<AccountItem> d, boolean more){ protected void onDataLoaded(List<AccountViewModel> d, boolean more){
if(refreshing){ if(refreshing){
relationships.clear(); relationships.clear();
} }
@ -89,7 +67,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<B
super.onRefresh(); super.onRefresh();
} }
protected void loadRelationships(List<AccountItem> accounts){ protected void loadRelationships(List<AccountViewModel> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet()); Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
GetAccountRelationships req=new GetAccountRelationships(ids); GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req); relationshipsRequests.add(req);
@ -125,9 +103,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<B
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
// list.setPadding(0, V.dp(16), 0, V.dp(16));
list.setClipToPadding(false); list.setClipToPadding(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 72, 16));
updateToolbar(); updateToolbar();
} }
@ -175,7 +151,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<B
@NonNull @NonNull
@Override @Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder(); return new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
} }
@Override @Override
@ -196,199 +172,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<B
@Override @Override
public ImageLoaderRequest getImageRequest(int position, int image){ public ImageLoaderRequest getImageRequest(int position, int image){
AccountItem item=data.get(position); AccountViewModel item=data.get(position);
return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1); return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1);
} }
} }
protected class AccountViewHolder extends BindableViewHolder<AccountItem> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name, username;
private final ImageView avatar;
private final Button button;
private final PopupMenu contextMenu;
private final View menuAnchor;
public AccountViewHolder(){
super(getActivity(), R.layout.item_account_list, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
avatar=findViewById(R.id.avatar);
button=findViewById(R.id.button);
menuAnchor=findViewById(R.id.menu_anchor);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true);
button.setOnClickListener(this::onButtonClick);
contextMenu=new PopupMenu(getActivity(), menuAnchor);
contextMenu.inflate(R.menu.profile);
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
}
@Override
public void onBind(AccountItem item){
name.setText(item.parsedName);
username.setText("@"+item.account.acct);
bindRelationship();
}
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(rel, button);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
@Override
public boolean onLongClick(){
return false;
}
@Override
public boolean onLongClick(float x, float y){
Relationship relationship=relationships.get(item.account.id);
if(relationship==null)
return false;
Menu menu=contextMenu.getMenu();
Account account=item.account;
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
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(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
if(relationship.following){
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
hideBoosts.setVisible(true);
}else{
hideBoosts.setVisible(false);
}
MenuItem blockDomain=menu.findItem(R.id.block_domain);
if(!account.isLocal()){
blockDomain.setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
blockDomain.setVisible(true);
}else{
blockDomain.setVisible(false);
}
menuAnchor.setTranslationX(x);
menuAnchor.setTranslationY(y);
contextMenu.show();
return true;
}
private void onButtonClick(View v){
ProgressDialog progress=new ProgressDialog(getActivity());
progress.setMessage(getString(R.string.loading));
progress.setCancelable(false);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationships.get(item.account.id), button, progressShown->{
itemView.setHasTransientState(progressShown);
if(progressShown)
progress.show();
else
progress.dismiss();
}, result->{
relationships.put(item.account.id, result);
bindRelationship();
});
}
private boolean onContextMenuItemSelected(MenuItem item){
Relationship relationship=relationships.get(this.item.account.id);
if(relationship==null)
return false;
Account account=this.item.account;
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}else if(id==R.id.report){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
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.getDomain(), relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
bindRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
relationships.put(AccountViewHolder.this.item.account.id, result);
bindRelationship();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
return true;
}
private void updateRelationship(Relationship r){
relationships.put(item.account.id, r);
bindRelationship();
}
}
protected static class AccountItem{
public final Account account;
public final ImageLoaderRequest avaRequest;
public final CustomEmojiHelper emojiHelper;
public final CharSequence parsedName;
public AccountItem(Account account){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
}
}
} }

View File

@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.account_list;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest; import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -23,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
nextMaxID=result.nextPageUri.getQueryParameter("max_id"); nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else else
nextMaxID=null; nextMaxID=null;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null); onDataLoaded(result.stream().map(AccountViewModel::new).collect(Collectors.toList()), nextMaxID!=null);
} }
}) })
.exec(accountID); .exec(accountID);

View File

@ -0,0 +1,34 @@
package org.joinmastodon.android.model.viewmodel;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class AccountViewModel{
public final Account account;
public final ImageLoaderRequest avaRequest;
public final CustomEmojiHelper emojiHelper;
public final CharSequence parsedName;
public final String verifiedLink;
public AccountViewModel(Account account){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
String verifiedLink=null;
for(AccountField fld:account.fields){
if(fld.verifiedAt!=null){
verifiedLink=HtmlParser.stripAndRemoveInvisibleSpans(fld.value);
break;
}
}
this.verifiedLink=verifiedLink;
}
}

View File

@ -31,7 +31,10 @@ public class CustomEmojiSpan extends ReplacementSpan{
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){ public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
int size=Math.round(paint.descent()-paint.ascent()); int size=Math.round(paint.descent()-paint.ascent());
if(drawable==null){ if(drawable==null){
canvas.drawRect(x, top, x+size, top+size, paint); int alpha=paint.getAlpha();
paint.setAlpha(alpha >> 1);
canvas.drawRoundRect(x, top, x+size, top+size, V.dp(2), V.dp(2), paint);
paint.setAlpha(alpha);
}else{ }else{
// AnimatedImageDrawable doesn't like when its bounds don't start at (0, 0) // AnimatedImageDrawable doesn't like when its bounds don't start at (0, 0)
Rect bounds=drawable.getBounds(); Rect bounds=drawable.getBounds();

View File

@ -12,6 +12,7 @@ import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Mention; import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node; import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode; import org.jsoup.nodes.TextNode;
@ -191,6 +192,13 @@ public class HtmlParser{
return Jsoup.clean(html, Safelist.none()); return Jsoup.clean(html, Safelist.none());
} }
public static String stripAndRemoveInvisibleSpans(String html){
Document doc=Jsoup.parseBodyFragment(html);
doc.body().select("span.invisible").remove();
Cleaner cleaner=new Cleaner(Safelist.none());
return cleaner.clean(doc).body().html();
}
public static CharSequence parseLinks(String text){ public static CharSequence parseLinks(String text){
Matcher matcher=URL_PATTERN.matcher(text); Matcher matcher=URL_PATTERN.matcher(text);
if(!matcher.find()) // Return the original string if there are no URLs if(!matcher.find()) // Return the original string if there are no URLs

View File

@ -0,0 +1,256 @@
package org.joinmastodon.android.ui.viewholders;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.style.TypefaceSpan;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.HashMap;
import java.util.Objects;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.utils.BindableViewHolder;
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;
private final ImageView avatar;
private final Button button;
private final PopupMenu contextMenu;
private final View menuAnchor;
private final TypefaceSpan mediumSpan=new TypefaceSpan("sans-serif-medium");
private final String accountID;
private final Fragment fragment;
private final HashMap<String, Relationship> relationships;
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment.getActivity(), R.layout.item_account_list, list);
this.fragment=fragment;
this.accountID=Objects.requireNonNull(fragment.getArguments().getString("account"));
this.relationships=relationships;
name=findViewById(R.id.name);
username=findViewById(R.id.username);
avatar=findViewById(R.id.avatar);
button=findViewById(R.id.button);
menuAnchor=findViewById(R.id.menu_anchor);
followers=findViewById(R.id.followers_count);
verifiedLink=findViewById(R.id.verified_link);
avatar.setOutlineProvider(OutlineProviders.roundedRect(16));
avatar.setClipToOutline(true);
button.setOnClickListener(this::onButtonClick);
contextMenu=new PopupMenu(fragment.getActivity(), menuAnchor);
contextMenu.inflate(R.menu.profile);
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
}
@SuppressLint("SetTextI18n")
@Override
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);
}
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();
}
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButtonM3(rel, button);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
if(index==0){
avatar.setImageResource(R.drawable.image_placeholder);
}else{
setImage(index, null);
}
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}
@Override
public boolean onLongClick(){
return false;
}
@Override
public boolean onLongClick(float x, float y){
Relationship relationship=relationships.get(item.account.id);
if(relationship==null)
return false;
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.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()));
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
if(relationship.following){
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
hideBoosts.setVisible(true);
}else{
hideBoosts.setVisible(false);
}
MenuItem blockDomain=menu.findItem(R.id.block_domain);
if(!account.isLocal()){
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
blockDomain.setVisible(true);
}else{
blockDomain.setVisible(false);
}
menuAnchor.setTranslationX(x);
menuAnchor.setTranslationY(y);
contextMenu.show();
return true;
}
private void onButtonClick(View v){
ProgressDialog progress=new ProgressDialog(fragment.getActivity());
progress.setMessage(fragment.getString(R.string.loading));
progress.setCancelable(false);
UiUtils.performAccountAction(fragment.getActivity(), item.account, accountID, relationships.get(item.account.id), button, progressShown->{
itemView.setHasTransientState(progressShown);
if(progressShown)
progress.show();
else
progress.dismiss();
}, result->{
relationships.put(item.account.id, result);
bindRelationship();
});
}
private boolean onContextMenuItemSelected(MenuItem item){
Relationship relationship=relationships.get(this.item.account.id);
if(relationship==null)
return false;
Account account=this.item.account;
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
fragment.startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(fragment.getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){
UiUtils.confirmToggleBlockUser(fragment.getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}else if(id==R.id.report){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
Nav.go(fragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(fragment.getActivity(), account.url);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(fragment.getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
bindRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
relationships.put(AccountViewHolder.this.item.account.id, result);
bindRelationship();
}
@Override
public void onError(ErrorResponse error){
error.showToast(fragment.getActivity());
}
})
.wrapProgress(fragment.getActivity(), R.string.loading, false)
.exec(accountID);
}
return true;
}
private void updateRelationship(Relationship r){
relationships.put(item.account.id, r);
bindRelationship();
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="@android:color/white"
android:pathData="M6.333,11.729 L3,8.396 4.062,7.333 6.333,9.604 11.938,4 13,5.062Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M10,15Q10.417,15 10.708,14.708Q11,14.417 11,14Q11,13.583 10.708,13.292Q10.417,13 10,13Q9.583,13 9.292,13.292Q9,13.583 9,14Q9,14.417 9.292,14.708Q9.583,15 10,15ZM9.25,11.812H10.771Q10.771,11.042 10.906,10.719Q11.042,10.396 11.562,9.896Q12.292,9.188 12.573,8.688Q12.854,8.188 12.854,7.583Q12.854,6.438 12.073,5.719Q11.292,5 10.083,5Q9.021,5 8.24,5.562Q7.458,6.125 7.146,7.083L8.5,7.646Q8.688,7.062 9.094,6.74Q9.5,6.417 10.042,6.417Q10.625,6.417 11,6.75Q11.375,7.083 11.375,7.625Q11.375,8.104 11.052,8.479Q10.729,8.854 10.333,9.208Q9.604,9.875 9.427,10.302Q9.25,10.729 9.25,11.812ZM10,18Q8.354,18 6.896,17.375Q5.438,16.75 4.344,15.656Q3.25,14.562 2.625,13.104Q2,11.646 2,10Q2,8.333 2.625,6.885Q3.25,5.438 4.344,4.344Q5.438,3.25 6.896,2.625Q8.354,2 10,2Q11.667,2 13.115,2.625Q14.562,3.25 15.656,4.344Q16.75,5.438 17.375,6.885Q18,8.333 18,10Q18,11.646 17.375,13.104Q16.75,14.562 15.656,15.656Q14.562,16.75 13.115,17.375Q11.667,18 10,18ZM10,16.5Q12.708,16.5 14.604,14.604Q16.5,12.708 16.5,10Q16.5,7.292 14.604,5.396Q12.708,3.5 10,3.5Q7.292,3.5 5.396,5.396Q3.5,7.292 3.5,10Q3.5,12.708 5.396,14.604Q7.292,16.5 10,16.5ZM10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorM3SurfaceVariant"/>
</shape>

View File

@ -2,55 +2,82 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="62dp" android:layout_height="72dp"
android:paddingLeft="16dp" android:paddingHorizontal="16dp"
android:paddingRight="16dp" android:paddingVertical="8dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:clipToPadding="false"> android:clipToPadding="false">
<ImageView <ImageView
android:id="@+id/avatar" android:id="@+id/avatar"
android:layout_width="46dp" android:layout_width="56dp"
android:layout_height="46dp" android:layout_height="56dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="8dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
tools:src="#0f0"/> tools:src="#0f0"/>
<Button <Button
android:id="@+id/button" android:id="@+id/button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="36dp"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_centerVertical="true" android:layout_alignParentTop="true"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
tools:text="Follow"/> tools:text="Follow"/>
<TextView <TextView
android:id="@+id/name" android:id="@+id/name"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="24dp" android:layout_height="20dp"
android:layout_toEndOf="@id/avatar" android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/button"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textAppearance="@style/m3_title_medium" android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurface"
tools:text="User"/> tools:text="User"/>
<TextView <TextView
android:id="@+id/username" android:id="@+id/username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="20dp" android:layout_height="20dp"
android:layout_below="@id/name" android:layout_marginStart="4dp"
android:layout_toEndOf="@id/avatar" android:layout_toEndOf="@id/name"
android:layout_toStartOf="@id/button" android:layout_toStartOf="@id/button"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textAppearance="@style/m3_title_small" android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3Secondary"
tools:text="\@user@server"/> tools:text="\@user@server"/>
<TextView
android:id="@+id/followers_count"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/avatar"
android:layout_below="@id/name"
android:layout_toStartOf="@id/button"
android:gravity="center_vertical"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3Secondary"
tools:text="123 followers"/>
<TextView
android:id="@+id/verified_link"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_toEndOf="@id/avatar"
android:layout_below="@id/followers_count"
android:layout_toStartOf="@id/button"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:includeFontPadding="false"
android:gravity="center_vertical"
android:drawablePadding="2dp"
tools:text="example.com/example"/>
<View <View
android:id="@+id/menu_anchor" android:id="@+id/menu_anchor"
android:layout_width="1px" android:layout_width="1px"

View File

@ -480,4 +480,5 @@
<string name="what_is_alt_text">What is alt text?</string> <string name="what_is_alt_text">What is alt text?</string>
<string name="alt_text_help">Alt text provides image descriptions for people with vision impairments, low-bandwidth connections, or those seeking extra context.\n\nYou can improve accessibility and understanding for everyone by writing clear, concise, and objective alt text.\n\n<ul><li>Capture important elements</li>\n<li>Summarize text in images</li>\n<li>Use regular sentence structure</li>\n<li>Avoid redundant information</li>\n<li>Focus on trends and key findings in complex visuals (like diagrams or maps)</li></ul></string> <string name="alt_text_help">Alt text provides image descriptions for people with vision impairments, low-bandwidth connections, or those seeking extra context.\n\nYou can improve accessibility and understanding for everyone by writing clear, concise, and objective alt text.\n\n<ul><li>Capture important elements</li>\n<li>Summarize text in images</li>\n<li>Use regular sentence structure</li>\n<li>Avoid redundant information</li>\n<li>Focus on trends and key findings in complex visuals (like diagrams or maps)</li></ul></string>
<string name="edit_post">Edit post</string> <string name="edit_post">Edit post</string>
<string name="no_verified_link">No verified link</string>
</resources> </resources>