@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import org.parceler.Parcels;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@@ -44,7 +45,7 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(String id){
|
public void onItemClick(String id){
|
||||||
UiUtils.openHashtagTimeline(getActivity(), accountID, id);
|
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,24 +1,52 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
import android.os.Bundle;
|
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.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
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.api.requests.timelines.GetHashtagTimeline;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.Status;
|
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 java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
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.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;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class HashtagTimelineFragment extends StatusListFragment{
|
public class HashtagTimelineFragment extends StatusListFragment{
|
||||||
private String hashtag;
|
private Hashtag hashtag;
|
||||||
|
private String hashtagName;
|
||||||
private ImageButton fab;
|
private ImageButton fab;
|
||||||
|
private TextView headerTitle, headerSubtitle;
|
||||||
|
private ProgressBarButton followButton;
|
||||||
|
private ProgressBar followProgress;
|
||||||
|
private MenuItem followMenuItem;
|
||||||
|
private boolean followRequestRunning;
|
||||||
|
private boolean toolbarContentVisible;
|
||||||
|
|
||||||
public HashtagTimelineFragment(){
|
public HashtagTimelineFragment(){
|
||||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||||
@@ -27,13 +55,19 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity){
|
public void onAttach(Activity activity){
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
hashtag=getArguments().getString("hashtag");
|
if(getArguments().containsKey("hashtag")){
|
||||||
setTitle('#'+hashtag);
|
hashtag=Parcels.unwrap(getArguments().getParcelable("hashtag"));
|
||||||
|
hashtagName=hashtag.name;
|
||||||
|
}else{
|
||||||
|
hashtagName=getArguments().getString("hashtagName");
|
||||||
|
}
|
||||||
|
setTitle('#'+hashtagName);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
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){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
@@ -50,17 +84,39 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
|||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadData(){
|
||||||
|
reloadTag();
|
||||||
|
super.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
fab=view.findViewById(R.id.fab);
|
fab=view.findViewById(R.id.fab);
|
||||||
fab.setOnClickListener(this::onFabClick);
|
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){
|
private void onFabClick(View v){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putString("prefilledText", '#'+hashtag+' ');
|
args.putString("prefilledText", '#'+hashtagName+' ');
|
||||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,4 +124,150 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
|||||||
protected void onSetFabBottomInset(int inset){
|
protected void onSetFabBottomInset(int inset){
|
||||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
|
|||||||
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
||||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
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 -> {
|
case STATUS -> {
|
||||||
Status status=res.status.getContentStatus();
|
Status status=res.status.getContentStatus();
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
|||||||
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
||||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
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 -> {
|
case STATUS -> {
|
||||||
Status status=res.status.getContentStatus();
|
Status status=res.status.getContentStatus();
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void openHashtag(SearchResult res){
|
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);
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(){
|
public void onClick(){
|
||||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
|
UiUtils.openHashtagTimeline(getActivity(), accountID, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ public class SignupFragment extends ToolbarFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void tail(Node node, int depth){
|
public void tail(Node node, int depth){
|
||||||
if(node instanceof Element){
|
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);
|
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class Hashtag extends BaseModel implements DisplayItemsParent{
|
|||||||
public String url;
|
public String url;
|
||||||
public List<History> history;
|
public List<History> history;
|
||||||
public int statusesCount;
|
public int statusesCount;
|
||||||
|
public boolean following;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(){
|
public String toString(){
|
||||||
@@ -21,6 +22,7 @@ public class Hashtag extends BaseModel implements DisplayItemsParent{
|
|||||||
", url='"+url+'\''+
|
", url='"+url+'\''+
|
||||||
", history="+history+
|
", history="+history+
|
||||||
", statusesCount="+statusesCount+
|
", statusesCount="+statusesCount+
|
||||||
|
", following="+following+
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ public class HtmlParser{
|
|||||||
Map<String, String> idsByUrl=mentions.stream().collect(Collectors.toMap(m->m.url, m->m.id));
|
Map<String, String> idsByUrl=mentions.stream().collect(Collectors.toMap(m->m.url, m->m.id));
|
||||||
// Hashtags in remote posts have remote URLs, these have local URLs so they don't match.
|
// Hashtags in remote posts have remote URLs, these have local URLs so they don't match.
|
||||||
// Map<String, String> tagsByUrl=tags.stream().collect(Collectors.toMap(t->t.url, t->t.name));
|
// Map<String, String> tagsByUrl=tags.stream().collect(Collectors.toMap(t->t.url, t->t.name));
|
||||||
|
Map<String, Hashtag> tagsByTag=tags.stream().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
|
||||||
|
|
||||||
final SpannableStringBuilder ssb=new SpannableStringBuilder();
|
final SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||||
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
|
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
|
||||||
@@ -96,6 +97,7 @@ public class HtmlParser{
|
|||||||
}else if(node instanceof Element el){
|
}else if(node instanceof Element el){
|
||||||
switch(el.nodeName()){
|
switch(el.nodeName()){
|
||||||
case "a" -> {
|
case "a" -> {
|
||||||
|
Object linkObject=null;
|
||||||
String href=el.attr("href");
|
String href=el.attr("href");
|
||||||
LinkSpan.Type linkType;
|
LinkSpan.Type linkType;
|
||||||
if(el.hasClass("hashtag")){
|
if(el.hasClass("hashtag")){
|
||||||
@@ -103,6 +105,7 @@ public class HtmlParser{
|
|||||||
if(text.startsWith("#")){
|
if(text.startsWith("#")){
|
||||||
linkType=LinkSpan.Type.HASHTAG;
|
linkType=LinkSpan.Type.HASHTAG;
|
||||||
href=text.substring(1);
|
href=text.substring(1);
|
||||||
|
linkObject=tagsByTag.get(text.substring(1).toLowerCase());
|
||||||
}else{
|
}else{
|
||||||
linkType=LinkSpan.Type.URL;
|
linkType=LinkSpan.Type.URL;
|
||||||
}
|
}
|
||||||
@@ -117,7 +120,7 @@ public class HtmlParser{
|
|||||||
}else{
|
}else{
|
||||||
linkType=LinkSpan.Type.URL;
|
linkType=LinkSpan.Type.URL;
|
||||||
}
|
}
|
||||||
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el));
|
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, linkObject), ssb.length(), el));
|
||||||
}
|
}
|
||||||
case "br" -> ssb.append('\n');
|
case "br" -> ssb.append('\n');
|
||||||
case "span" -> {
|
case "span" -> {
|
||||||
@@ -213,7 +216,7 @@ public class HtmlParser{
|
|||||||
String url=matcher.group(3);
|
String url=matcher.group(3);
|
||||||
if(TextUtils.isEmpty(matcher.group(4)))
|
if(TextUtils.isEmpty(matcher.group(4)))
|
||||||
url="http://"+url;
|
url="http://"+url;
|
||||||
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0);
|
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, null), matcher.start(3), matcher.end(3), 0);
|
||||||
}while(matcher.find()); // Find more URLs
|
}while(matcher.find()); // Find more URLs
|
||||||
return ssb;
|
return ssb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
|||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
import android.text.style.CharacterStyle;
|
import android.text.style.CharacterStyle;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
public class LinkSpan extends CharacterStyle {
|
public class LinkSpan extends CharacterStyle {
|
||||||
@@ -13,12 +14,14 @@ public class LinkSpan extends CharacterStyle {
|
|||||||
private String link;
|
private String link;
|
||||||
private Type type;
|
private Type type;
|
||||||
private String accountID;
|
private String accountID;
|
||||||
|
private Object linkObject;
|
||||||
|
|
||||||
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){
|
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, Object linkObject){
|
||||||
this.listener=listener;
|
this.listener=listener;
|
||||||
this.link=link;
|
this.link=link;
|
||||||
this.type=type;
|
this.type=type;
|
||||||
this.accountID=accountID;
|
this.accountID=accountID;
|
||||||
|
this.linkObject=linkObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getColor(){
|
public int getColor(){
|
||||||
@@ -35,7 +38,12 @@ public class LinkSpan extends CharacterStyle {
|
|||||||
switch(getType()){
|
switch(getType()){
|
||||||
case URL -> UiUtils.openURL(context, accountID, link);
|
case URL -> UiUtils.openURL(context, accountID, link);
|
||||||
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
|
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
|
||||||
case HASHTAG -> UiUtils.openHashtagTimeline(context, accountID, link);
|
case HASHTAG -> {
|
||||||
|
if(linkObject instanceof Hashtag ht)
|
||||||
|
UiUtils.openHashtagTimeline(context, accountID, ht);
|
||||||
|
else
|
||||||
|
UiUtils.openHashtagTimeline(context, accountID, link);
|
||||||
|
}
|
||||||
case CUSTOM -> listener.onLinkClick(this);
|
case CUSTOM -> listener.onLinkClick(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import org.joinmastodon.android.fragments.ProfileFragment;
|
|||||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
@@ -342,10 +343,17 @@ public class UiUtils{
|
|||||||
Nav.go((Activity)context, ProfileFragment.class, args);
|
Nav.go((Activity)context, ProfileFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void openHashtagTimeline(Context context, String accountID, Hashtag hashtag){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("hashtag", Parcels.wrap(hashtag));
|
||||||
|
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
public static void openHashtagTimeline(Context context, String accountID, String hashtag){
|
public static void openHashtagTimeline(Context context, String accountID, String hashtag){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putString("hashtag", hashtag);
|
args.putString("hashtagName", hashtag);
|
||||||
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
|
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
62
mastodon/src/main/res/layout/header_hashtag_timeline.xml
Normal file
62
mastodon/src/main/res/layout/header_hashtag_timeline.xml
Normal 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="wrap_content"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_toStartOf="@id/follow_btn_wrap"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAppearance="@style/m3_headline_small"
|
||||||
|
android:textColor="?colorM3OnSurface"
|
||||||
|
android:maxLines="4"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:minHeight="36dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
tools:text="#CatsOfMastodonButLong"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/follow_btn_wrap"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignTop="@id/title"
|
||||||
|
android:layout_alignBottom="@id/title"
|
||||||
|
android:layout_alignParentEnd="true">
|
||||||
|
|
||||||
|
<org.joinmastodon.android.ui.views.ProgressBarButton
|
||||||
|
android:id="@+id/profile_action_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
style="@style/Widget.Mastodon.M3.Button.Filled"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
tools:text="@string/button_follow" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/action_progress"
|
||||||
|
style="?android:progressBarStyleSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:elevation="10dp"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:outlineProvider="none"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/title"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textAppearance="@style/m3_label_large"
|
||||||
|
android:textColor="?colorM3OnSurfaceVariant"
|
||||||
|
tools:text="123 posts"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
@@ -596,4 +596,13 @@
|
|||||||
<string name="post_translated">Translated from %1$s using %2$s</string>
|
<string name="post_translated">Translated from %1$s using %2$s</string>
|
||||||
<string name="translation_show_original">Show original</string>
|
<string name="translation_show_original">Show original</string>
|
||||||
<string name="translation_failed">Translation failed. Maybe the administrator has not enabled translations on this server or this server is running an older version of Mastodon where translations are not yet supported.</string>
|
<string name="translation_failed">Translation failed. Maybe the administrator has not enabled translations on this server or this server is running an older version of Mastodon where translations are not yet supported.</string>
|
||||||
|
|
||||||
|
<plurals name="x_participants">
|
||||||
|
<item quantity="one">%,d participant</item>
|
||||||
|
<item quantity="other">%,d participants</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="x_posts_today">
|
||||||
|
<item quantity="one">%,d post today</item>
|
||||||
|
<item quantity="other">%,d posts today</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user