Add following/followers lists

closes #25
This commit is contained in:
Grishka 2022-05-02 05:45:51 +03:00
parent 7b26649521
commit 02a1f2ef8c
16 changed files with 786 additions and 85 deletions

View File

@ -58,7 +58,7 @@ dependencies {
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.4'
implementation 'me.grishka.appkit:appkit:1.2.6'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@ -1,14 +1,10 @@
package org.joinmastodon.android.api;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
@ -16,11 +12,9 @@ import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.BaseModel;
import java.io.IOException;
import java.io.Reader;
@ -144,7 +138,7 @@ public class MastodonAPIController{
}
try{
req.validateAndPostprocessResponse(respObj);
req.validateAndPostprocessResponse(respObj, response);
}catch(IOException x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);

View File

@ -28,6 +28,7 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import okhttp3.Call;
import okhttp3.RequestBody;
import okhttp3.Response;
public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
private static final String TAG="MastodonAPIRequest";
@ -158,7 +159,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
@CallSuper
public void validateAndPostprocessResponse(T respObj) throws IOException{
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){
((BaseModel) respObj).postprocess();
}else if(respObj instanceof List){

View File

@ -0,0 +1,57 @@
package org.joinmastodon.android.api.requests;
import android.net.Uri;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.HeaderPaginationList;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.Response;
public abstract class HeaderPaginationRequest<I> extends MastodonAPIRequest<HeaderPaginationList<I>>{
private static final Pattern LINK_HEADER_PATTERN=Pattern.compile("(?:(?:,\\s*)?<([^>]+)>|;\\s*(\\w+)=['\"](\\w+)['\"])");
public HeaderPaginationRequest(HttpMethod method, String path, Class<HeaderPaginationList<I>> respClass){
super(method, path, respClass);
}
public HeaderPaginationRequest(HttpMethod method, String path, TypeToken<HeaderPaginationList<I>> respTypeToken){
super(method, path, respTypeToken);
}
@Override
public void validateAndPostprocessResponse(HeaderPaginationList<I> respObj, Response httpResponse) throws IOException{
super.validateAndPostprocessResponse(respObj, httpResponse);
String link=httpResponse.header("Link");
if(!TextUtils.isEmpty(link)){
Matcher matcher=LINK_HEADER_PATTERN.matcher(link);
String url=null;
while(matcher.find()){
if(url==null){
String _url=matcher.group(1);
if(_url==null)
continue;
url=_url;
}else{
String paramName=matcher.group(2);
String paramValue=matcher.group(3);
if(paramName==null || paramValue==null)
return;
if("rel".equals(paramName)){
switch(paramValue){
case "next" -> respObj.nextPageUri=Uri.parse(url);
case "prev" -> respObj.prevPageUri=Uri.parse(url);
}
url=null;
}
}
}
}
}
}

View File

@ -0,0 +1,16 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account;
public class GetAccountFollowers extends HeaderPaginationRequest<Account>{
public GetAccountFollowers(String id, String maxID, int limit){
super(HttpMethod.GET, "/accounts/"+id+"/followers", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@ -0,0 +1,16 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account;
public class GetAccountFollowing extends HeaderPaginationRequest<Account>{
public GetAccountFollowing(String id, String maxID, int limit){
super(HttpMethod.GET, "/accounts/"+id+"/following", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@ -0,0 +1,381 @@
package org.joinmastodon.android.fragments;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
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 org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship;
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.parceler.Parcels;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
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.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseAccountListFragment.AccountItem>{
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
public BaseAccountListFragment(){
super(40);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
}
@Override
protected void onDataLoaded(List<AccountItem> d, boolean more){
if(refreshing){
relationships.clear();
}
loadRelationships(d);
super.onDataLoaded(d, more);
}
@Override
public void onRefresh(){
for(APIRequest<?> req:relationshipsRequests){
req.cancel();
}
relationshipsRequests.clear();
super.onRefresh();
}
protected void loadRelationships(List<AccountItem> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req);
req.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationshipsRequests.remove(req);
for(Relationship rel:result){
relationships.put(rel.id, rel);
}
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof AccountViewHolder avh){
avh.bindRelationship();
}
}
}
@Override
public void onError(ErrorResponse error){
relationshipsRequests.remove(req);
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter getAdapter(){
return new AccountsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
// list.setPadding(0, V.dp(16), 0, V.dp(16));
list.setClipToPadding(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 72, 16));
updateToolbar();
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
}
@CallSuper
protected void updateToolbar(){
Toolbar toolbar=getToolbar();
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
toolbar.setNavigationContentDescription(R.string.back);
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
toolbar.setTitleTextColor(color);
toolbar.setSubtitleTextColor(color);
}
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, V.dp(16), 0, V.dp(16));
}
super.onApplyWindowInsets(insets);
}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder();
}
@Override
public void onBindViewHolder(AccountViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getImageCountForItem(int position){
return data.get(position).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountItem item=data.get(position);
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){
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()));
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, account.getDisplayUsername()));
else
menu.findItem(R.id.hide_boosts).setVisible(false);
if(!account.isLocal())
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
else
menu.findItem(R.id.block_domain).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

@ -0,0 +1,49 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowers;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.parceler.Parcels;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class FollowerListFragment extends BaseAccountListFragment{
private Account account;
private String nextMaxID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setTitle("@"+account.acct);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, account.followersCount, account.followersCount));
}
@Override
public void onResume(){
super.onResume();
if(!loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountFollowers(account.id, offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
}
})
.exec(accountID);
}
}

View File

@ -0,0 +1,49 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.parceler.Parcels;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class FollowingListFragment extends BaseAccountListFragment{
private Account account;
private String nextMaxID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setTitle("@"+account.acct);
setSubtitle(getResources().getQuantityString(R.plurals.x_following, account.followingCount, account.followingCount));
}
@Override
public void onResume(){
super.onResume();
if(!loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountFollowing(account.id, offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
}
})
.exec(accountID);
}
}

View File

@ -261,6 +261,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
fab.setVisibility(View.GONE);
}
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
return sizeWrapper;
}
@ -848,6 +851,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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 class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@ -0,0 +1,24 @@
package org.joinmastodon.android.model;
import android.net.Uri;
import java.util.ArrayList;
import java.util.Collection;
import androidx.annotation.NonNull;
public class HeaderPaginationList<T> extends ArrayList<T>{
public Uri nextPageUri, prevPageUri;
public HeaderPaginationList(int initialCapacity){
super(initialCapacity);
}
public HeaderPaginationList(){
super();
}
public HeaderPaginationList(@NonNull Collection<? extends T> c){
super(c);
}
}

View File

@ -136,8 +136,10 @@ public class AccountSwitcherSheet extends BottomSheet{
if(tappableBottom==0 && insetBottom>0){
list.setPadding(0, 0, 0, V.dp(48)-insetBottom);
}else{
list.setPadding(0, 0, 0, 0);
list.setPadding(0, 0, 0, V.dp(24));
}
}else{
list.setPadding(0, 0, 0, V.dp(24));
}
}

View File

@ -133,18 +133,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29){
DisplayCutout cutout=insets.getDisplayCutout();
Insets tappable=insets.getTappableElementInsets();
if(cutout!=null){
// Make controls extend beneath the cutout, and replace insets to avoid cutout insets being filled with "navigation bar color"
Insets tappable=insets.getTappableElementInsets();
int leftInset=Math.max(0, cutout.getSafeInsetLeft()-tappable.left);
int rightInset=Math.max(0, cutout.getSafeInsetRight()-tappable.right);
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
toolbarWrap.setPadding(leftInset, 0, rightInset, 0);
videoControls.setPadding(leftInset, 0, rightInset, 0);
}else{
toolbarWrap.setPadding(0, 0, 0, 0);
videoControls.setPadding(0, 0, 0, 0);
}
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
}
uiOverlay.dispatchApplyWindowInsets(insets);
int bottomInset=insets.getSystemWindowInsetBottom();

View File

@ -75,80 +75,95 @@
tools:src="#0f0" />
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_alignParentEnd="true"
android:id="@+id/profile_counters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/cover"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:padding="4dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/following_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/following_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
android:layout_toEndOf="@id/avatar_border"
android:gravity="end">
<LinearLayout
android:id="@+id/followers_btn"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_toStartOf="@id/following_btn"
android:layout_below="@id/cover"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:padding="4dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/followers_count"
<LinearLayout
android:id="@+id/posts_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/followers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
android:layout_height="56dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="4dp">
<TextView
android:id="@+id/posts_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
android:singleLine="true"
android:ellipsize="end"
tools:text="123" />
<TextView
android:id="@+id/posts_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
android:singleLine="true"
android:ellipsize="middle"
tools:text="posts" />
</LinearLayout>
<LinearLayout
android:id="@+id/posts_btn"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_below="@id/cover"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_toStartOf="@id/followers_btn"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="4dp">
<TextView
android:id="@+id/posts_count"
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123" />
android:layout_height="56dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:padding="4dp"
android:orientation="vertical"
android:background="?android:selectableItemBackgroundBorderless"
android:gravity="center_horizontal">
<TextView
android:id="@+id/following_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
android:singleLine="true"
android:ellipsize="end"
tools:text="123"/>
<TextView
android:id="@+id/following_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
android:singleLine="true"
android:ellipsize="middle"
tools:text="following"/>
</LinearLayout>
<TextView
android:id="@+id/posts_label"
<LinearLayout
android:id="@+id/followers_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following" />
android:layout_height="56dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:padding="4dp"
android:orientation="vertical"
android:background="?android:selectableItemBackgroundBorderless"
android:gravity="center_horizontal">
<TextView
android:id="@+id/followers_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
android:singleLine="true"
android:ellipsize="end"
tools:text="123"/>
<TextView
android:id="@+id/followers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
android:singleLine="true"
android:ellipsize="middle"
tools:text="followers"/>
</LinearLayout>
</LinearLayout>
<FrameLayout
@ -156,7 +171,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/following_btn"
android:layout_below="@id/profile_counters"
android:padding="16dp"
android:clipToPadding="false">
<org.joinmastodon.android.ui.views.ProgressBarButton

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="62dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/avatar"
android:layout_width="46dp"
android:layout_height="46dp"
android:layout_marginEnd="10dp"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="8dp"
tools:text="Follow"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/button"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_medium"
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/button"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_small"
tools:text="\@user@server"/>
<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"/>
</RelativeLayout>

View File

@ -11,14 +11,14 @@
<string name="ok">OK</string>
<string name="preparing_auth">Preparing for authentication…</string>
<string name="finishing_auth">Finishing authentication…</string>
<string name="user_boosted">%s boosted</string>
<string name="user_boosted">%s reblogged</string>
<string name="in_reply_to">In reply to %s</string>
<string name="notifications">Notifications</string>
<string name="user_followed_you">followed you</string>
<string name="user_sent_follow_request">sent you a follow request</string>
<string name="user_favorited">favorited your toot</string>
<string name="notification_boosted">boosted your toot</string>
<string name="notification_boosted">reblogged your post</string>
<string name="poll_ended">poll ended</string>
<string name="time_seconds">%ds</string>
@ -227,7 +227,7 @@
<string name="skip">Skip</string>
<string name="notification_type_follow">New followers</string>
<string name="notification_type_favorite">Favorites</string>
<string name="notification_type_reblog">Boosts</string>
<string name="notification_type_reblog">Reblogs</string>
<string name="notification_type_mention">Mentions</string>
<string name="notification_type_poll">Polls</string>
<string name="choose_account">Choose account</string>
@ -290,8 +290,8 @@
<string name="unfollowed_user">Unfollowed %s</string>
<string name="followed_user">You\'re now following %s</string>
<string name="open_in_browser">Open in browser</string>
<string name="hide_boosts_from_user">Hide boosts from %s</string>
<string name="show_boosts_from_user">Show boosts from %s</string>
<string name="hide_boosts_from_user">Hide reblogs from %s</string>
<string name="show_boosts_from_user">Show reblogs from %s</string>
<string name="signup_reason">why do you want to join?</string>
<string name="signup_reason_note">This will help us review your application.</string>
<string name="clear">Clear</string>
@ -320,4 +320,22 @@
<string name="manually_approves_followers">Manually approves followers</string>
<string name="current_account">Current account</string>
<string name="log_out_account">Log Out %s</string>
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
<plurals name="x_followers">
<item quantity="one">%,d follower</item>
<item quantity="other">%,d followers</item>
</plurals>
<plurals name="x_following">
<item quantity="one">%,d following</item>
<item quantity="other">%,d following</item>
</plurals>
<plurals name="x_favorites">
<item quantity="one">%,d favorite</item>
<item quantity="other">%,d favorites</item>
</plurals>
<plurals name="x_reblogs">
<item quantity="one">%,d reblog</item>
<item quantity="other">%,d reblogs</item>
</plurals>
</resources>