Add favorites/reblogs lists and extended footer for ThreadFragment

closes #41, closes #64
This commit is contained in:
Grishka 2022-05-04 01:04:59 +03:00
parent e512a7ef90
commit 23ec3e64cf
24 changed files with 379 additions and 87 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 31
versionCode 35
versionName "1.0.4"
versionCode 36
versionName "1.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -58,6 +58,7 @@ public class StatusInteractionController{
status.favouritesCount++;
else
status.favouritesCount--;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged){
@ -95,5 +96,6 @@ public class StatusInteractionController{
status.reblogsCount++;
else
status.reblogsCount--;
E.post(new StatusCountersUpdatedEvent(status));
}
}

View File

@ -0,0 +1,16 @@
package org.joinmastodon.android.api.requests.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account;
public class GetStatusFavorites extends HeaderPaginationRequest<Account>{
public GetStatusFavorites(String id, String maxID, int limit){
super(HttpMethod.GET, "/statuses/"+id+"/favourited_by", 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.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account;
public class GetStatusReblogs extends HeaderPaginationRequest<Account>{
public GetStatusReblogs(String id, String maxID, int limit){
super(HttpMethod.GET, "/statuses/"+id+"/reblogged_by", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@ -31,6 +31,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.TileGridLayoutManager;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
@ -662,7 +663,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}

View File

@ -1,49 +0,0 @@
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

@ -45,6 +45,8 @@ import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;

View File

@ -10,6 +10,7 @@ import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@ -90,16 +91,15 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
footer.rebind();
return;
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
footer.rebind();
}
}
return;
}
}
for(Status s:preloadedData){
if(s.id.equals(ev.id)){
s.update(ev);
return;
}
}
}

View File

@ -11,6 +11,8 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
@ -45,7 +47,10 @@ public class ThreadFragment extends StatusListFragment{
for(StatusDisplayItem item:items){
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true;
}
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
}
return items;
}

View File

@ -0,0 +1,17 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.model.Account;
import org.parceler.Parcels;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
protected Account account;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setTitle("@"+account.acct);
}
}

View File

@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.account_list;
import android.app.ProgressDialog;
import android.content.Intent;
@ -7,7 +7,6 @@ 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;
@ -23,6 +22,7 @@ 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.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship;
@ -141,6 +141,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
Toolbar toolbar=getToolbar();
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
toolbar.setNavigationContentDescription(R.string.back);
if(hasSubtitle()){
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);
@ -148,6 +149,11 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
toolbar.setSubtitleTextColor(color);
}
}
}
protected boolean hasSubtitle(){
return true;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){

View File

@ -0,0 +1,22 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowers;
import org.joinmastodon.android.model.Account;
public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, account.followersCount, account.followersCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowers(account.id, maxID, count);
}
}

View File

@ -0,0 +1,22 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
import org.joinmastodon.android.model.Account;
public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_following, account.followingCount, account.followingCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowing(account.id, maxID, count);
}
}

View File

@ -1,39 +1,21 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowers;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
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;
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{
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();
}
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountFollowers(account.id, offset==0 ? null : nextMaxID, count)
currentRequest=onCreateRequest(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
@ -46,4 +28,11 @@ public class FollowerListFragment extends BaseAccountListFragment{
})
.exec(accountID);
}
@Override
public void onResume(){
super.onResume();
if(!loaded && !dataLoading)
loadData();
}
}

View File

@ -0,0 +1,21 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
import org.joinmastodon.android.model.Account;
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(getResources().getQuantityString(R.plurals.x_favorites, status.favouritesCount, status.favouritesCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusFavorites(status.id, maxID, count);
}
}

View File

@ -0,0 +1,21 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
import org.joinmastodon.android.model.Account;
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, status.reblogsCount, status.reblogsCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusReblogs(status.id, maxID, count);
}
}

View File

@ -0,0 +1,21 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{
protected Status status;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
status=Parcels.unwrap(getArguments().getParcelable("status"));
}
@Override
protected boolean hasSubtitle(){
return false;
}
}

View File

@ -0,0 +1,112 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.TypefaceSpan;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import androidx.annotation.PluralsRes;
import me.grishka.appkit.Nav;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
super(parentID, parentFragment);
this.status=status;
}
@Override
public Type getType(){
return Type.EXTENDED_FOOTER;
}
public static class Holder extends StatusDisplayItem.Holder<ExtendedFooterStatusDisplayItem>{
private final TextView reblogs, favorites, time;
private final View buttonsView;
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_extended_footer, parent);
reblogs=findViewById(R.id.reblogs);
favorites=findViewById(R.id.favorites);
time=findViewById(R.id.timestamp);
buttonsView=findViewById(R.id.button_bar);
reblogs.setOnClickListener(v->startAccountListFragment(StatusReblogsListFragment.class));
favorites.setOnClickListener(v->startAccountListFragment(StatusFavoritesListFragment.class));
}
@Override
public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status;
if(s.favouritesCount>0){
favorites.setVisibility(View.VISIBLE);
favorites.setText(getFormattedPlural(R.plurals.x_favorites, s.favouritesCount));
}else{
favorites.setVisibility(View.GONE);
}
if(s.reblogsCount>0){
reblogs.setVisibility(View.VISIBLE);
reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, s.reblogsCount));
}else{
reblogs.setVisibility(View.GONE);
}
if(s.favouritesCount==0 && s.reblogsCount==0){
buttonsView.setVisibility(View.GONE);
}else{
buttonsView.setVisibility(View.VISIBLE);
}
String timeStr=TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault()));
if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){
time.setText(item.parentFragment.getString(R.string.timestamp_via_app, timeStr, item.status.application.name));
}else{
time.setText(timeStr);
}
}
@Override
public boolean isEnabled(){
return false;
}
private SpannableStringBuilder getFormattedPlural(@PluralsRes int res, int quantity){
String str=item.parentFragment.getResources().getQuantityString(res, quantity, quantity);
String formattedNumber=String.format(Locale.getDefault(), "%,d", quantity);
int index=str.indexOf(formattedNumber);
SpannableStringBuilder ssb=new SpannableStringBuilder(str);
if(index>=0){
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), index, index+formattedNumber.length(), 0);
ssb.setSpan(new ForegroundColorSpan(UiUtils.getThemeColor(item.parentFragment.getActivity(), android.R.attr.textColorPrimary)), index, index+formattedNumber.length(), 0);
}
return ssb;
}
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), cls, args);
}
}
}

View File

@ -29,6 +29,7 @@ import me.grishka.appkit.utils.V;
public class FooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
private final String accountID;
public boolean hideCounts;
public FooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, String accountID){
super(parentID, parentFragment);
@ -91,7 +92,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void bindButton(TextView btn, int count){
if(count>0){
if(count>0 && !item.hideCounts){
btn.setText(DecimalFormat.getIntegerInstance().format(count));
btn.setCompoundDrawablePadding(V.dp(8));
}else{

View File

@ -64,6 +64,7 @@ public abstract class StatusDisplayItem{
case ACCOUNT -> new AccountStatusDisplayItem.Holder(activity, parent);
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
};
}
@ -146,7 +147,8 @@ public abstract class StatusDisplayItem{
ACCOUNT_CARD,
ACCOUNT,
HASHTAG,
GAP
GAP,
EXTENDED_FOOTER
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{

View File

@ -10,6 +10,7 @@ import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="4dp"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,54 @@
<?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="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLightest">
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/reblogs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="8dp"
android:textSize="14sp"
android:minHeight="36dp"
android:textColor="?android:textColorSecondary"
android:background="@drawable/bg_text_button"
android:fontFamily="sans-serif"
tools:text="4 reblogs"/>
<Button
android:id="@+id/favorites"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="8dp"
android:textSize="14sp"
android:minHeight="36dp"
android:textColor="?android:textColorSecondary"
android:background="@drawable/bg_text_button"
android:fontFamily="sans-serif"
tools:text="12 favorites"/>
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
<TextView
android:id="@+id/timestamp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:minHeight="20dp"
android:gravity="center_vertical"
android:textSize="14sp"
android:textColor="?android:textColorSecondary"
tools:text="Dec 12, 2021, 12:42 PM via Mastodon for Android"/>
</LinearLayout>

View File

@ -338,4 +338,5 @@
<item quantity="one">%,d reblog</item>
<item quantity="other">%,d reblogs</item>
</plurals>
<string name="timestamp_via_app">%1$s via %2$s</string>
</resources>