Skip to content

Commit

Permalink
Hashtag following
Browse files Browse the repository at this point in the history
closes #684, closes #233
  • Loading branch information
grishka committed Sep 12, 2023
1 parent 209e603 commit 6d84f28
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.tags;

import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;

public class GetTag extends MastodonAPIRequest<Hashtag>{
public GetTag(String tag){
super(HttpMethod.GET, "/tags/"+tag, Hashtag.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.tags;

import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;

public class SetTagFollowed extends MastodonAPIRequest<Hashtag>{
public SetTagFollowed(String tag, boolean followed){
super(HttpMethod.POST, "/tags/"+tag+(followed ? "/follow" : "/unfollow"), Hashtag.class);
setRequestBody(new Object());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import androidx.recyclerview.widget.RecyclerView;
Expand Down Expand Up @@ -44,7 +45,7 @@ protected void addAccountToKnown(Hashtag s){

@Override
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, id);
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
package org.joinmastodon.android.fragments;

import android.app.Activity;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;

import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.tags.GetTag;
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.SpacerSpan;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.parceler.Parcels;

import java.util.List;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;

public class HashtagTimelineFragment extends StatusListFragment{
private String hashtag;
private Hashtag hashtag;
private String hashtagName;
private ImageButton fab;
private TextView headerTitle, headerSubtitle;
private ProgressBarButton followButton;
private ProgressBar followProgress;
private MenuItem followMenuItem;
private boolean followRequestRunning;
private boolean toolbarContentVisible;

public HashtagTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
Expand All @@ -27,13 +55,19 @@ public HashtagTimelineFragment(){
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
hashtag=getArguments().getString("hashtag");
setTitle('#'+hashtag);
if(getArguments().containsKey("hashtag")){
hashtag=Parcels.unwrap(getArguments().getParcelable("hashtag"));
hashtagName=hashtag.name;
}else{
hashtagName=getArguments().getString("hashtagName");
}
setTitle('#'+hashtagName);
setHasOptionsMenu(true);
}

@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)
currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : getMaxID(), null, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
Expand All @@ -50,22 +84,190 @@ protected void onShown(){
loadData();
}

@Override
public void loadData(){
reloadTag();
super.loadData();
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);

list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
View topChild=recyclerView.getChildAt(0);
int firstChildPos=recyclerView.getChildAdapterPosition(topChild);
float newAlpha=firstChildPos>0 ? 1f : Math.min(1f, -topChild.getTop()/(float)headerTitle.getHeight());
toolbarTitleView.setAlpha(newAlpha);
boolean newToolbarVisibility=newAlpha>0.5f;
if(newToolbarVisibility!=toolbarContentVisible){
toolbarContentVisible=newToolbarVisibility;
if(followMenuItem!=null)
followMenuItem.setVisible(toolbarContentVisible);
}
}
});
}

private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", '#'+hashtag+' ');
args.putString("prefilledText", '#'+hashtagName+' ');
Nav.go(getActivity(), ComposeFragment.class, args);
}

@Override
protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
}

@Override
protected RecyclerView.Adapter getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.header_hashtag_timeline, list, false);
headerTitle=header.findViewById(R.id.title);
headerSubtitle=header.findViewById(R.id.subtitle);
followButton=header.findViewById(R.id.profile_action_btn);
followProgress=header.findViewById(R.id.action_progress);

headerTitle.setText("#"+hashtagName);
followButton.setVisibility(View.GONE);
followButton.setOnClickListener(v->{
if(hashtag==null)
return;
setFollowed(!hashtag.following);
});
updateHeader();

MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(header));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}

@Override
protected int getMainAdapterOffset(){
return 1;
}

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
followMenuItem=menu.add(getString(hashtag!=null && hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName));
followMenuItem.setVisible(toolbarContentVisible);
}

@Override
public boolean onOptionsItemSelected(MenuItem item){
if(hashtag!=null){
setFollowed(!hashtag.following);
}
return true;
}

@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
toolbarTitleView.setAlpha(toolbarContentVisible ? 1f : 0f);
if(followMenuItem!=null)
followMenuItem.setVisible(toolbarContentVisible);
}

private void updateHeader(){
if(hashtag==null)
return;

if(hashtag.history!=null && !hashtag.history.isEmpty()){
int weekPosts=hashtag.history.stream().mapToInt(h->h.uses).sum();
int todayPosts=hashtag.history.get(0).uses;
int numAccounts=hashtag.history.stream().mapToInt(h->h.accounts).sum();
int hSpace=V.dp(8);
SpannableStringBuilder ssb=new SpannableStringBuilder();
ssb.append(getResources().getQuantityString(R.plurals.x_posts, weekPosts, weekPosts));
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append('·');
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append(getResources().getQuantityString(R.plurals.x_participants, numAccounts, numAccounts));
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append('·');
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append(getResources().getQuantityString(R.plurals.x_posts_today, todayPosts, todayPosts));
headerSubtitle.setText(ssb);
}

int styleRes;
followButton.setVisibility(View.VISIBLE);
if(hashtag.following){
followButton.setText(R.string.button_following);
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal;
}else{
followButton.setText(R.string.button_follow);
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}
TypedArray ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
followButton.setBackground(ta.getDrawable(0));
ta.recycle();
ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
followButton.setTextColor(ta.getColorStateList(0));
followProgress.setIndeterminateTintList(ta.getColorStateList(0));
ta.recycle();

followButton.setTextVisible(true);
followProgress.setVisibility(View.GONE);
if(followMenuItem!=null){
followMenuItem.setTitle(getString(hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName));
}
}

private void reloadTag(){
new GetTag(hashtagName)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Hashtag result){
hashtag=result;
updateHeader();
}

@Override
public void onError(ErrorResponse error){

}
})
.exec(accountID);
}

private void setFollowed(boolean followed){
if(followRequestRunning)
return;
followButton.setTextVisible(false);
followProgress.setVisibility(View.VISIBLE);
followRequestRunning=true;
new SetTagFollowed(hashtagName, followed)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Hashtag result){
if(getActivity()==null)
return;
hashtag=result;
updateHeader();
followRequestRunning=false;
}

@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
if(error instanceof MastodonErrorResponse er && "Duplicate record".equals(er.error)){
hashtag.following=true;
}else{
error.showToast(getActivity());
}
updateHeader();
followRequestRunning=false;
}
})
.exec(accountID);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public void onItemClick(String id){
args.putParcelable("profileAccount", Parcels.wrap(res.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag);
case STATUS -> {
Status status=res.status.getContentStatus();
Bundle args=new Bundle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void onItemClick(String id){
args.putParcelable("profileAccount", Parcels.wrap(res.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag);
case STATUS -> {
Status status=res.status.getContentStatus();
Bundle args=new Bundle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ public void onAnimationEnd(Animator animation){
}

private void openHashtag(SearchResult res){
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public void onBind(Hashtag item){

@Override
public void onClick(){
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
UiUtils.openHashtagTimeline(getActivity(), accountID, item);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public void head(Node node, int depth){
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class Hashtag extends BaseModel implements DisplayItemsParent{
public String url;
public List<History> history;
public int statusesCount;
public boolean following;

@Override
public String toString(){
Expand All @@ -21,6 +22,7 @@ public String toString(){
", url='"+url+'\''+
", history="+history+
", statusesCount="+statusesCount+
", following="+following+
'}';
}

Expand Down
Loading

0 comments on commit 6d84f28

Please sign in to comment.