Featured tab in profiles

This commit is contained in:
Grishka 2023-03-22 02:30:42 +03:00
parent 09ffda2605
commit 955b9a4b2b
21 changed files with 498 additions and 80 deletions

View File

@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
import java.util.List;
public class GetAccountFeaturedHashtags extends MastodonAPIRequest<List<Hashtag>>{
public GetAccountFeaturedHashtags(String id){
super(HttpMethod.GET, "/accounts/"+id+"/featured_tags", new TypeToken<>(){});
}
}

View File

@ -27,6 +27,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
addQueryParameter("exclude_reblogs", "true");
}
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
case PINNED -> addQueryParameter("pinned", "true");
}
}
@ -35,6 +36,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
INCLUDE_REPLIES,
MEDIA,
NO_REBLOGS,
OWN_POSTS_AND_REPLIES
OWN_POSTS_AND_REPLIES,
PINNED
}
}

View File

@ -0,0 +1,57 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag>{
private Account account;
private String accountID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
onDataLoaded(getArguments().getParcelableArrayList("hashtags").stream().map(p->(Hashtag)Parcels.unwrap(p)).collect(Collectors.toList()), false);
setTitle(R.string.featured_hashtags);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Hashtag s){
return Collections.singletonList(new HashtagStatusDisplayItem(s.name, this, s));
}
@Override
protected void addAccountToKnown(Hashtag s){
}
@Override
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, id);
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
// no-op
}
}

View File

@ -0,0 +1,36 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class PinnedPostsListFragment extends StatusListFragment{
private Account account;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
setTitle(R.string.pinned_posts);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
new GetAccountStatuses(account.id, null, null, 100, GetAccountStatuses.Filter.PINNED)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, false);
}
}).exec(accountID);
}
}

View File

@ -0,0 +1,199 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFeaturedHashtags;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SectionHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult>{
private Account profileAccount;
private List<Hashtag> featuredTags;
// private List<Account> endorsedAccounts;
private List<Status> pinnedStatuses;
private boolean tagsLoaded, statusesLoaded;
public ProfileFeaturedFragment(){
setListLayoutId(R.layout.recycler_fragment_no_refresh);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
profileAccount=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(SearchResult s){
ArrayList<StatusDisplayItem> items=switch(s.type){
case ACCOUNT -> new ArrayList<>(Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)));
case HASHTAG -> new ArrayList<>(Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
};
if(s.firstInSection){
items.add(0, new SectionHeaderStatusDisplayItem(this, getString(switch(s.type){
case ACCOUNT -> R.string.profile_endorsed_accounts;
case HASHTAG -> R.string.hashtags;
case STATUS -> R.string.posts;
}), getString(R.string.view_all), switch(s.type){
case ACCOUNT -> (Runnable)this::showAllEndorsedAccounts;
case HASHTAG -> (Runnable)this::showAllFeaturedHashtags;
case STATUS -> (Runnable)this::showAllPinnedPosts;
}));
}
return items;
}
@Override
protected void addAccountToKnown(SearchResult s){
Account acc=switch(s.type){
case ACCOUNT -> s.account;
case STATUS -> s.status.account;
case HASHTAG -> null;
};
if(acc!=null && !knownAccounts.containsKey(acc.id))
knownAccounts.put(acc.id, acc);
}
@Override
public void onItemClick(String id){
SearchResult res=getResultByID(id);
if(res==null)
return;
switch(res.type){
case ACCOUNT -> {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(res.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
case STATUS -> {
Status status=res.status.getContentStatus();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}
}
}
@Override
protected void doLoadData(int offset, int count){
if(!statusesLoaded){
new GetAccountStatuses(profileAccount.id, null, null, 1, GetAccountStatuses.Filter.PINNED)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
pinnedStatuses=result;
statusesLoaded=true;
onOneApiRequestCompleted();
}
})
.exec(accountID);
}
if(!tagsLoaded){
new GetAccountFeaturedHashtags(profileAccount.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Hashtag> result){
featuredTags=result;
tagsLoaded=true;
onOneApiRequestCompleted();
}
})
.exec(accountID);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onRefresh(){
statusesLoaded=false;
tagsLoaded=false;
super.onRefresh();
}
private void onOneApiRequestCompleted(){
if(tagsLoaded && statusesLoaded){
ArrayList<SearchResult> results=new ArrayList<>();
if(!pinnedStatuses.isEmpty()){
SearchResult res=new SearchResult(pinnedStatuses.get(0));
res.firstInSection=true;
results.add(res);
}
for(int i=0;i<Math.min(3, featuredTags.size());i++){
SearchResult res=new SearchResult(featuredTags.get(i));
res.firstInSection=(i==0);
results.add(res);
}
onDataLoaded(results, false);
}
}
protected SearchResult getResultByID(String id){
for(SearchResult s:data){
if(s.id.equals(id)){
return s;
}
}
return null;
}
@Override
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
// no-op
}
private void showAllPinnedPosts(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
Nav.go(getActivity(), PinnedPostsListFragment.class, args);
}
private void showAllFeaturedHashtags(){
Bundle args=new Bundle();
args.putString("account", accountID);
ArrayList<Parcelable> tags=featuredTags.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new));
args.putParcelableArrayList("hashtags", tags);
Nav.go(getActivity(), FeaturedHashtagsListFragment.class, args);
}
private void showAllEndorsedAccounts(){
}
}

View File

@ -18,6 +18,7 @@ import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
@ -111,7 +112,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ProgressBarButton actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private ProfileFeaturedFragment featuredFragment;
private AccountTimelineFragment timelineFragment;
private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
@ -216,14 +218,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
};
tabViews=new FrameLayout[4];
tabViews=new FrameLayout[3];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.profile_posts;
case 1 -> R.id.profile_posts_with_replies;
case 2 -> R.id.profile_media;
case 3 -> R.id.profile_about;
case 0 -> R.id.profile_featured;
case 1 -> R.id.profile_timeline;
case 2 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@ -245,10 +246,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies;
case 2 -> R.string.media;
case 3 -> R.string.profile_about;
case 0 -> R.string.profile_featured;
case 1 -> R.string.profile_timeline;
case 2 -> R.string.profile_about;
default -> throw new IllegalStateException();
});
}
@ -312,12 +312,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
if(timelineFragment.loaded)
timelineFragment.onRefresh();
if(featuredFragment.loaded)
featuredFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
}
@ -337,10 +335,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public void dataLoaded(){
if(getActivity()==null)
return;
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
if(featuredFragment==null){
featuredFragment=new ProfileFeaturedFragment();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
args.putBoolean("__is_tab", true);
featuredFragment.setArguments(args);
timelineFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
}
@ -397,11 +399,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
});
}
@Override
public void onDestroyView(){
super.onDestroyView();
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
@ -425,10 +422,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void applyChildWindowInsets(){
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets);
if(timelineFragment!=null && timelineFragment.isAdded() && childInsets!=null){
timelineFragment.onApplyWindowInsets(childInsets);
featuredFragment.onApplyWindowInsets(childInsets);
}
}
@ -693,10 +689,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private Fragment getFragmentForPage(int page){
return switch(page){
case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment;
case 3 -> aboutFragment;
case 0 -> featuredFragment;
case 1 -> timelineFragment;
case 2 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
@ -759,7 +754,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.save_changes);
pager.setCurrentItem(3);
pager.setCurrentItem(2);
for(int i=0;i<3;i++){
tabbar.getTabAt(i).view.setEnabled(false);
}
@ -1001,7 +996,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public int getItemCount(){
return loaded ? 4 : 0;
return loaded ? 3 : 0;
}
@Override

View File

@ -6,12 +6,13 @@ import org.parceler.Parcel;
import java.util.List;
@Parcel
public class Hashtag extends BaseModel{
public class Hashtag extends BaseModel implements DisplayItemsParent{
@RequiredField
public String name;
@RequiredField
public String url;
public List<History> history;
public int statusesCount;
@Override
public String toString(){
@ -19,6 +20,12 @@ public class Hashtag extends BaseModel{
"name='"+name+'\''+
", url='"+url+'\''+
", history="+history+
", statusesCount="+statusesCount+
'}';
}
@Override
public String getID(){
return name;
}
}

View File

@ -11,6 +11,7 @@ public class SearchResult extends BaseModel implements DisplayItemsParent{
public Type type;
public transient String id;
public transient boolean firstInSection;
public SearchResult(){}

View File

@ -37,12 +37,15 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public void onBind(HashtagStatusDisplayItem _item){
Hashtag item=_item.tag;
title.setText('#'+item.name);
if(item.history!=null && !item.history.isEmpty()){
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
subtitle.setText(_item.parentFragment.getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
subtitle.setText(itemView.getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
chart.setData(item.history);
}else{
subtitle.setText(itemView.getResources().getQuantityString(R.plurals.x_posts, item.statusesCount, item.statusesCount));
}
}
}
}

View File

@ -0,0 +1,55 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
public class SectionHeaderStatusDisplayItem extends StatusDisplayItem{
public final String title, buttonText;
public final Runnable onButtonClick;
public SectionHeaderStatusDisplayItem(BaseStatusListFragment parentFragment, String title, String buttonText, Runnable onButtonClick){
super("", parentFragment);
this.title=title;
this.buttonText=buttonText;
this.onButtonClick=onButtonClick;
}
@Override
public Type getType(){
return Type.SECTION_HEADER;
}
public static class Holder extends StatusDisplayItem.Holder<SectionHeaderStatusDisplayItem>{
private final TextView title;
private final Button actionBtn;
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_section_header, parent);
title=findViewById(R.id.title);
actionBtn=findViewById(R.id.action_btn);
actionBtn.setOnClickListener(v->item.onButtonClick.run());
}
@Override
public void onBind(SectionHeaderStatusDisplayItem item){
title.setText(item.title);
if(item.onButtonClick!=null){
actionBtn.setVisibility(View.VISIBLE);
actionBtn.setText(item.buttonText);
}else{
actionBtn.setVisibility(View.GONE);
}
}
@Override
public boolean isEnabled(){
return false;
}
}
}

View File

@ -65,6 +65,7 @@ public abstract class StatusDisplayItem{
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
case SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent);
case SECTION_HEADER -> new SectionHeaderStatusDisplayItem.Holder(activity, parent);
};
}
@ -154,7 +155,8 @@ public abstract class StatusDisplayItem{
GAP,
EXTENDED_FOOTER,
MEDIA_GRID,
SPOILER
SPOILER,
SECTION_HEADER
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{

View File

@ -14,12 +14,12 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.utils.CustomViewHelper;
public class HashtagChartView extends View{
public class HashtagChartView extends View implements CustomViewHelper{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private Path strokePath=new Path(), fillPath=new Path();
private CornerPathEffect pathEffect=new CornerPathEffect(V.dp(3));
private final CornerPathEffect pathEffect=new CornerPathEffect(dp(3));
private float[] relativeOffsets=new float[7];
public HashtagChartView(Context context){
@ -32,7 +32,7 @@ public class HashtagChartView extends View{
public HashtagChartView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
paint.setStrokeWidth(V.dp(1.71f));
paint.setStrokeWidth(dp(1.71f));
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
}
@ -57,20 +57,20 @@ public class HashtagChartView extends View{
return;
strokePath.rewind();
fillPath.rewind();
float step=(getWidth()-V.dp(2))/(float)(relativeOffsets.length-1);
float maxH=getHeight()-V.dp(2);
float x=getWidth()-V.dp(1);
strokePath.moveTo(x, maxH-maxH*relativeOffsets[0]+V.dp(1));
fillPath.moveTo(getWidth(), getHeight()-V.dp(1));
fillPath.lineTo(x, maxH-maxH*relativeOffsets[0]+V.dp(1));
float step=(getWidth()-dp(2))/(float)(relativeOffsets.length-1);
float maxH=getHeight()-dp(2);
float x=getWidth()-dp(1);
strokePath.moveTo(x, maxH-maxH*relativeOffsets[0]+dp(1));
fillPath.moveTo(getWidth(), getHeight()-dp(1));
fillPath.lineTo(x, maxH-maxH*relativeOffsets[0]+dp(1));
for(int i=1;i<relativeOffsets.length;i++){
float offset=relativeOffsets[i];
x-=step;
float y=maxH-maxH*offset+V.dp(1);
float y=maxH-maxH*offset+dp(1);
strokePath.lineTo(x, y);
fillPath.lineTo(x, y);
}
fillPath.lineTo(V.dp(1), getHeight()-V.dp(1));
fillPath.lineTo(dp(1), getHeight()-dp(1));
fillPath.close();
}
@ -83,11 +83,11 @@ public class HashtagChartView extends View{
@Override
protected void onDraw(Canvas canvas){
paint.setStyle(Paint.Style.FILL);
paint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorAccentLightest));
paint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3PrimaryInverse));
paint.setPathEffect(null);
canvas.drawPath(fillPath, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(UiUtils.getThemeColor(getContext(), android.R.attr.colorAccent));
paint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
paint.setPathEffect(pathEffect);
canvas.drawPath(strokePath, paint);
}

View File

@ -26,8 +26,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
final RecyclerView rv = (RecyclerView) target;
if ((dy < 0 && isScrolledToTop(rv)) || (dy > 0 && !isScrolledToBottom())) {
if(target instanceof RecyclerView rv && ((dy < 0 && isScrolledToTop(rv)) || (dy > 0 && !isScrolledToBottom()))){
scrollBy(0, dy);
consumed[1] = dy;
return;
@ -37,8 +36,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
@Override
public boolean onNestedPreFling(View target, float velX, float velY) {
final RecyclerView rv = (RecyclerView) target;
if ((velY < 0 && isScrolledToTop(rv)) || (velY > 0 && !isScrolledToBottom())) {
if (target instanceof RecyclerView rv && ((velY < 0 && isScrolledToTop(rv)) || (velY > 0 && !isScrolledToBottom()))){
fling((int) velY);
return true;
}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:baselineAligned="false">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="24dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_medium"
android:textColor="?colorM3OnSurface"
android:singleLine="true"
android:ellipsize="end"
tools:text="Section Header"/>
<Button
android:id="@+id/action_btn"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginEnd="-8dp"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3Primary"
android:background="@drawable/bg_button_borderless_rounded"
android:paddingLeft="8dp"
android:paddingRight="8dp"
tools:text="Action"/>
</LinearLayout>

View File

@ -2,11 +2,7 @@
<FrameLayout 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:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp">
android:layout_height="wrap_content">
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/text"
@ -14,6 +10,10 @@
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?colorM3OnSurface"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:textAppearance="@style/m3_body_large"/>
</FrameLayout>

View File

@ -215,11 +215,11 @@
android:layout_height="match_parent"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:fontFamily="sans-serif-black"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?colorM3OnSurfaceVariant"
android:textSize="14dp"
android:textStyle="bold"
tools:text="123" />
<TextView

View File

@ -2,39 +2,43 @@
<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="64dp"
android:layout_height="72dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="12dp">
android:paddingTop="14dp">
<org.joinmastodon.android.ui.views.HashtagChartView
android:id="@+id/chart"
android:layout_width="64dp"
android:layout_height="32dp"
android:layout_width="66dp"
android:layout_height="36dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="7dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="16dp"
android:layout_alignParentEnd="true"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="24dp"
android:layout_toStartOf="@id/chart"
android:textAppearance="@style/m3_title_medium"
android:textAppearance="@style/m3_body_large"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textColor="?colorM3OnSurface"
tools:text="#mastodev"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="20dp"
android:layout_below="@id/title"
android:layout_toStartOf="@id/chart"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="over 9000 people talking"/>
</RelativeLayout>

View File

@ -41,6 +41,7 @@
<attr name="colorM3OnError" format="color"/>
<attr name="colorM3ErrorContainer" format="color"/>
<attr name="colorM3OnErrorContainer" format="color"/>
<attr name="colorM3PrimaryInverse" format="color"/>
<attr name="primaryLargeButtonStyle" format="reference"/>
<attr name="secondaryLargeButtonStyle" format="reference"/>

View File

@ -2,9 +2,8 @@
<resources>
<item name="header" type="id"/>
<item name="profile_posts" type="id"/>
<item name="profile_posts_with_replies" type="id"/>
<item name="profile_media" type="id"/>
<item name="profile_featured" type="id"/>
<item name="profile_timeline" type="id"/>
<item name="profile_about" type="id"/>
<item name="discover_posts" type="id"/>

View File

@ -442,4 +442,10 @@
<string name="spoiler_hide">Re-hide</string>
<string name="poll_multiple_choice">Choose one or more</string>
<string name="save_changes">Save changes</string>
<string name="profile_featured">Featured</string>
<string name="profile_timeline">Timeline</string>
<string name="view_all">View all</string>
<string name="profile_endorsed_accounts">Accounts</string>
<string name="pinned_posts">Pinned posts</string>
<string name="featured_hashtags">Featured hashtags</string>
</resources>

View File

@ -65,6 +65,7 @@
<item name="colorM3OnError">#FFF</item>
<item name="colorM3ErrorContainer">#F9DEDC</item>
<item name="colorM3OnErrorContainer">#410E0B</item>
<item name="colorM3PrimaryInverse">@color/m3_sys_dark_primary</item>
<item name="colorWindowBackground">?colorM3Background</item>
<item name="android:statusBarColor">?colorM3Background</item>
@ -139,6 +140,7 @@
<item name="colorM3OnError">#601410</item>
<item name="colorM3ErrorContainer">#8C1D18</item>
<item name="colorM3OnErrorContainer">#F9DEDC</item>
<item name="colorM3PrimaryInverse">@color/m3_sys_light_primary</item>
<item name="colorWindowBackground">?colorM3Background</item>
<item name="android:statusBarColor">?colorM3Background</item>