diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetFollowSuggestions.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetFollowSuggestions.java new file mode 100644 index 00000000..a6004708 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetFollowSuggestions.java @@ -0,0 +1,20 @@ +package org.joinmastodon.android.api.requests.accounts; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.FollowSuggestion; + +import java.util.List; + +public class GetFollowSuggestions extends MastodonAPIRequest>{ + public GetFollowSuggestions(int limit){ + super(HttpMethod.GET, "/suggestions", new TypeToken<>(){}); + addQueryParameter("limit", limit+""); + } + + @Override + protected String getPathPrefix(){ + return "/api/v2"; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/GetCustomEmojis.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetCustomEmojis.java similarity index 85% rename from mastodon/src/main/java/org/joinmastodon/android/api/requests/GetCustomEmojis.java rename to mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetCustomEmojis.java index 9a70a477..f91eb669 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/GetCustomEmojis.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetCustomEmojis.java @@ -1,4 +1,4 @@ -package org.joinmastodon.android.api.requests; +package org.joinmastodon.android.api.requests.instance; import com.google.gson.reflect.TypeToken; diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/GetInstance.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstance.java similarity index 81% rename from mastodon/src/main/java/org/joinmastodon/android/api/requests/GetInstance.java rename to mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstance.java index 4d5d24f0..84273ce2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/GetInstance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstance.java @@ -1,4 +1,4 @@ -package org.joinmastodon.android.api.requests; +package org.joinmastodon.android.api.requests.instance; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Instance; diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingHashtags.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingHashtags.java new file mode 100644 index 00000000..82b368ad --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingHashtags.java @@ -0,0 +1,15 @@ +package org.joinmastodon.android.api.requests.trends; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Hashtag; + +import java.util.List; + +public class GetTrendingHashtags extends MastodonAPIRequest>{ + public GetTrendingHashtags(int limit){ + super(HttpMethod.GET, "/trends", new TypeToken<>(){}); + addQueryParameter("limit", limit+""); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingLinks.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingLinks.java new file mode 100644 index 00000000..eeea39b3 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingLinks.java @@ -0,0 +1,14 @@ +package org.joinmastodon.android.api.requests.trends; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Card; + +import java.util.List; + +public class GetTrendingLinks extends MastodonAPIRequest>{ + public GetTrendingLinks(){ + super(HttpMethod.GET, "/trends/links", new TypeToken<>(){}); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingStatuses.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingStatuses.java index 7c947954..b902563c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingStatuses.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/trends/GetTrendingStatuses.java @@ -8,12 +8,8 @@ import org.joinmastodon.android.model.Status; import java.util.List; public class GetTrendingStatuses extends MastodonAPIRequest>{ - public GetTrendingStatuses(String maxID, String minID, int limit){ + public GetTrendingStatuses(int limit){ super(HttpMethod.GET, "/trends/statuses", new TypeToken<>(){}); - if(maxID!=null) - addQueryParameter("max_id", maxID); - if(minID!=null) - addQueryParameter("min_id", minID); if(limit>0) addQueryParameter("limit", ""+limit); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 273c0bf1..fcb719ed 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -11,7 +11,7 @@ import com.google.gson.JsonParseException; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIController; -import org.joinmastodon.android.api.requests.GetCustomEmojis; +import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.model.Account; @@ -34,7 +34,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverAccountsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverAccountsFragment.java new file mode 100644 index 00000000..efe52fd0 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverAccountsFragment.java @@ -0,0 +1,282 @@ +package org.joinmastodon.android.fragments; + +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; +import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.FollowSuggestion; +import org.joinmastodon.android.model.Relationship; +import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.ProgressBarButton; +import org.parceler.Parcels; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +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.fragments.BaseRecyclerFragment; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; +import me.grishka.appkit.views.UsableRecyclerView; + +public class DiscoverAccountsFragment extends BaseRecyclerFragment{ + private String accountID; + private Map relationships=Collections.emptyMap(); + private GetAccountRelationships relationshipsRequest; + + public DiscoverAccountsFragment(){ + super(20); + } + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + accountID=getArguments().getString("account"); + } + + @Override + protected void doLoadData(int offset, int count){ + if(relationshipsRequest!=null){ + relationshipsRequest.cancel(); + relationshipsRequest=null; + } + currentRequest=new GetFollowSuggestions(count) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(List result){ + onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).collect(Collectors.toList()), false); + loadRelationships(); + } + }) + .exec(accountID); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + return new AccountsAdapter(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + list.addItemDecoration(new RecyclerView.ItemDecoration(){ + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + outRect.bottom=outRect.left=outRect.right=V.dp(16); + if(parent.getChildAdapterPosition(view)==0) + outRect.top=V.dp(16); + } + }); + ((UsableRecyclerView)list).setDrawSelectorOnTop(true); + } + + private void loadRelationships(){ + relationships=Collections.emptyMap(); + relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList())); + relationshipsRequest.setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + relationshipsRequest=null; + relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity())); + for(int i=0;i implements ImageLoaderRecyclerAdapter{ + + public AccountsAdapter(){ + super(imgLoader); + } + + @Override + public void onBindViewHolder(AccountViewHolder holder, int position){ + holder.bind(data.get(position)); + super.onBindViewHolder(holder, position); + } + + @NonNull + @Override + public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new AccountViewHolder(); + } + + @Override + public int getItemCount(){ + return data.size(); + } + + @Override + public int getImageCountForItem(int position){ + return 2+data.get(position).emojiHelper.getImageCount(); + } + + @Override + public ImageLoaderRequest getImageRequest(int position, int image){ + AccountWrapper item=data.get(position); + if(image==0) + return item.avaRequest; + else if(image==1) + return item.coverRequest; + else + return item.emojiHelper.getImageRequest(image-2); + } + } + + private class AccountViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{ + private final ImageView cover, avatar; + private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel; + private final ProgressBarButton actionButton; + private final ProgressBar actionProgress; + private final View actionWrap; + + private Relationship relationship; + + public AccountViewHolder(){ + super(getActivity(), R.layout.item_discover_account, list); + cover=findViewById(R.id.cover); + avatar=findViewById(R.id.avatar); + name=findViewById(R.id.name); + username=findViewById(R.id.username); + bio=findViewById(R.id.bio); + followersCount=findViewById(R.id.followers_count); + followersLabel=findViewById(R.id.followers_label); + followingCount=findViewById(R.id.following_count); + followingLabel=findViewById(R.id.following_label); + postsCount=findViewById(R.id.posts_count); + postsLabel=findViewById(R.id.posts_label); + actionButton=findViewById(R.id.action_btn); + actionProgress=findViewById(R.id.action_progress); + actionWrap=findViewById(R.id.action_btn_wrap); + + itemView.setOutlineProvider(OutlineProviders.roundedRect(6)); + itemView.setClipToOutline(true); + avatar.setOutlineProvider(OutlineProviders.roundedRect(12)); + avatar.setClipToOutline(true); + cover.setOutlineProvider(OutlineProviders.roundedRect(3)); + cover.setClipToOutline(true); + actionButton.setOnClickListener(this::onActionButtonClick); + } + + @Override + public void onBind(AccountWrapper item){ + name.setText(item.parsedName); + username.setText('@'+item.account.acct); + bio.setText(item.parsedBio); + followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount)); + followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount)); + postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount)); + followersLabel.setText(getResources().getQuantityString(R.plurals.followers, Math.min(999, item.account.followersCount))); + followingLabel.setText(getResources().getQuantityString(R.plurals.following, Math.min(999, item.account.followingCount))); + postsLabel.setText(getResources().getQuantityString(R.plurals.posts, Math.min(999, item.account.statusesCount))); + relationship=relationships.get(item.account.id); + if(relationship==null){ + actionWrap.setVisibility(View.GONE); + }else{ + actionWrap.setVisibility(View.VISIBLE); + UiUtils.setRelationshipToActionButton(relationship, actionButton); + } + } + + @Override + public void setImage(int index, Drawable image){ + if(index==0){ + avatar.setImageDrawable(image); + }else if(index==1){ + cover.setImageDrawable(image); + }else{ + item.emojiHelper.setImageDrawable(index-2, image); + name.invalidate(); + bio.invalidate(); + } + if(image instanceof Animatable && !((Animatable) image).isRunning()) + ((Animatable) image).start(); + } + + @Override + public void clearImage(int index){ + setImage(index, null); + } + + @Override + public void onClick(){ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("profileAccount", Parcels.wrap(item.account)); + Nav.go(getActivity(), ProfileFragment.class, args); + } + + private void onActionButtonClick(View v){ + itemView.setHasTransientState(true); + UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{ + itemView.setHasTransientState(false); + relationships.put(item.account.id, rel); + rebind(); + }); + } + + private void setActionProgressVisible(boolean visible){ + actionButton.setTextVisible(!visible); + actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE); + actionButton.setClickable(!visible); + } + } + + protected class AccountWrapper{ + public Account account; + public ImageLoaderRequest avaRequest, coverRequest; + public CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); + public CharSequence parsedName, parsedBio; + + public AccountWrapper(Account account){ + this.account=account; + if(!TextUtils.isEmpty(account.avatar)) + avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50)); + if(!TextUtils.isEmpty(account.header)) + coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000); + parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), accountID); + if(account.emojis.isEmpty()){ + parsedName=account.displayName; + }else{ + parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); + emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); + } + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverFragment.java new file mode 100644 index 00000000..ed6cfdc7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverFragment.java @@ -0,0 +1,178 @@ +package org.joinmastodon.android.fragments; + +import android.app.Fragment; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.ui.SimpleViewHolder; +import org.joinmastodon.android.ui.tabs.TabLayout; +import org.joinmastodon.android.ui.tabs.TabLayoutMediator; +import org.joinmastodon.android.ui.utils.UiUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; +import me.grishka.appkit.fragments.AppKitFragment; +import me.grishka.appkit.fragments.BaseRecyclerFragment; +import me.grishka.appkit.utils.V; + +public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{ + + private TabLayout tabLayout; + private ViewPager2 pager; + private FrameLayout[] tabViews; + private TabLayoutMediator tabLayoutMediator; + + private DiscoverPostsFragment postsFragment; + private TrendingHashtagsFragment hashtagsFragment; + private DiscoverNewsFragment newsFragment; + private DiscoverAccountsFragment accountsFragment; + + private String accountID; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) + setRetainInstance(true); + + accountID=getArguments().getString("account"); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){ + LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_discover, container, false); + + tabLayout=view.findViewById(R.id.tabbar); + pager=view.findViewById(R.id.pager); + + tabViews=new FrameLayout[4]; + for(int i=0;i R.id.discover_posts; + case 1 -> R.id.discover_hashtags; + case 2 -> R.id.discover_news; + case 3 -> R.id.discover_users; + default -> throw new IllegalStateException("Unexpected value: "+i); + }); + tabView.setVisibility(View.GONE); + view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment + tabViews[i]=tabView; + } + + tabLayout.setTabTextSize(V.dp(16)); + tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); + + pager.setOffscreenPageLimit(4); + pager.setAdapter(new DiscoverPagerAdapter()); + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){ + @Override + public void onPageSelected(int position){ + if(position==0) + return; + Fragment _page=getFragmentForPage(position); + if(_page instanceof BaseRecyclerFragment){ + BaseRecyclerFragment page=(BaseRecyclerFragment) _page; + if(!page.loaded && !page.isDataLoading()) + page.loadData(); + } + } + }); + + if(postsFragment==null){ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putBoolean("__is_tab", true); + + postsFragment=new DiscoverPostsFragment(); + postsFragment.setArguments(args); + + hashtagsFragment=new TrendingHashtagsFragment(); + hashtagsFragment.setArguments(args); + + newsFragment=new DiscoverNewsFragment(); + newsFragment.setArguments(args); + + accountsFragment=new DiscoverAccountsFragment(); + accountsFragment.setArguments(args); + + getChildFragmentManager().beginTransaction() + .add(R.id.discover_posts, postsFragment) + .add(R.id.discover_hashtags, hashtagsFragment) + .add(R.id.discover_news, newsFragment) + .add(R.id.discover_users, accountsFragment) + .commit(); + } + + tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){ + @Override + public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ + tab.setText(switch(position){ + case 0 -> R.string.posts; + case 1 -> R.string.hashtags; + case 2 -> R.string.news; + case 3 -> R.string.for_you; + default -> throw new IllegalStateException("Unexpected value: "+position); + }); + tab.view.textView.setAllCaps(true); + } + }); + tabLayoutMediator.attach(); + + return view; + } + + @Override + public void scrollToTop(){ + + } + + public void loadData(){ + if(postsFragment!=null && !postsFragment.loaded && !postsFragment.dataLoading) + postsFragment.loadData(); + } + + private Fragment getFragmentForPage(int page){ + return switch(page){ + case 0 -> postsFragment; + case 1 -> hashtagsFragment; + case 2 -> newsFragment; + case 3 -> accountsFragment; + default -> throw new IllegalStateException("Unexpected value: "+page); + }; + } + + private class DiscoverPagerAdapter extends RecyclerView.Adapter{ + @NonNull + @Override + public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + FrameLayout view=tabViews[viewType]; + ((ViewGroup)view.getParent()).removeView(view); + view.setVisibility(View.VISIBLE); + view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return new SimpleViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){} + + @Override + public int getItemCount(){ + return 4; + } + + @Override + public int getItemViewType(int position){ + return position; + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverNewsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverNewsFragment.java new file mode 100644 index 00000000..195694e7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverNewsFragment.java @@ -0,0 +1,158 @@ +package org.joinmastodon.android.fragments; + +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.trends.GetTrendingLinks; +import org.joinmastodon.android.model.Card; +import org.joinmastodon.android.ui.DividerItemDecoration; +import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; +import org.joinmastodon.android.ui.utils.UiUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.fragments.BaseRecyclerFragment; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; +import me.grishka.appkit.views.UsableRecyclerView; + +public class DiscoverNewsFragment extends BaseRecyclerFragment{ + private String accountID; + private List imageRequests=Collections.emptyList(); + + public DiscoverNewsFragment(){ + super(10); + } + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + accountID=getArguments().getString("account"); + } + + @Override + protected void doLoadData(int offset, int count){ + currentRequest=new GetTrendingLinks() + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(List result){ + imageRequests=result.stream() + .map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150))) + .collect(Collectors.toList()); + onDataLoaded(result, false); + } + }) + .exec(accountID); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + return new LinksAdapter(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0)); + } + + private class LinksAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ + public LinksAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + public LinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new LinkViewHolder(); + } + + @Override + public int getItemCount(){ + return data.size(); + } + + @Override + public void onBindViewHolder(LinkViewHolder holder, int position){ + holder.bind(data.get(position)); + super.onBindViewHolder(holder, position); + } + + @Override + public int getImageCountForItem(int position){ + return imageRequests.get(position)==null ? 0 : 1; + } + + @Override + public ImageLoaderRequest getImageRequest(int position, int image){ + return imageRequests.get(position); + } + } + + private class LinkViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{ + private final TextView name, title, subtitle; + private final ImageView photo; + private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); + private boolean didClear; + + public LinkViewHolder(){ + super(getActivity(), R.layout.item_trending_link, list); + name=findViewById(R.id.name); + title=findViewById(R.id.title); + subtitle=findViewById(R.id.subtitle); + photo=findViewById(R.id.photo); + photo.setOutlineProvider(OutlineProviders.roundedRect(2)); + photo.setClipToOutline(true); + } + + @Override + public void onBind(Card item){ + name.setText(item.providerName); + title.setText(item.title); + int num=item.history.get(0).uses; + if(item.history.size()>1) + num+=item.history.get(1).uses; + subtitle.setText(getResources().getQuantityString(R.plurals.discussed_x_times, num, num)); + crossfadeDrawable.setSize(item.width, item.height); + crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder); + crossfadeDrawable.setCrossfadeAlpha(0f); + photo.setImageDrawable(null); + photo.setImageDrawable(crossfadeDrawable); + didClear=false; + } + + @Override + public void setImage(int index, Drawable drawable){ + crossfadeDrawable.setImageDrawable(drawable); + if(didClear) + crossfadeDrawable.animateAlpha(0f); + } + + @Override + public void clearImage(int index){ + crossfadeDrawable.setCrossfadeAlpha(1f); + didClear=true; + } + + @Override + public void onClick(){ + UiUtils.launchWebBrowser(getActivity(), item.url); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SearchFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverPostsFragment.java similarity index 81% rename from mastodon/src/main/java/org/joinmastodon/android/fragments/SearchFragment.java rename to mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverPostsFragment.java index 4dccbe49..1640f407 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SearchFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/DiscoverPostsFragment.java @@ -11,14 +11,14 @@ import java.util.List; import me.grishka.appkit.api.SimpleCallback; -public class SearchFragment extends StatusListFragment{ +public class DiscoverPostsFragment extends StatusListFragment{ @Override protected void doLoadData(int offset, int count){ - currentRequest=new GetTrendingStatuses(offset>0 ? getMaxID() : null, null, count) + currentRequest=new GetTrendingStatuses(count) .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(List result){ - onDataLoaded(result, !result.isEmpty()); + onDataLoaded(result, false); } }).exec(accountID); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 1e021e3f..4ab18352 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -36,7 +36,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene private FragmentRootLinearLayout content; private HomeTimelineFragment homeTimelineFragment; private NotificationsFragment notificationsFragment; - private SearchFragment searchFragment; + private DiscoverFragment searchFragment; private ProfileFragment profileFragment; private TabBar tabBar; private View tabBarWrap; @@ -60,7 +60,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene homeTimelineFragment.setArguments(args); args=new Bundle(args); args.putBoolean("noAutoLoad", true); - searchFragment=new SearchFragment(); + searchFragment=new DiscoverFragment(); searchFragment.setArguments(args); notificationsFragment=new NotificationsFragment(); notificationsFragment.setArguments(args); @@ -168,6 +168,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene LoaderFragment lf=(LoaderFragment) newFragment; if(!lf.loaded && !lf.dataLoading) lf.loadData(); + }else if(newFragment instanceof DiscoverFragment){ + ((DiscoverFragment) newFragment).loadData(); } currentTab=tab; ((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index b1d47d7b..db469bed 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -5,7 +5,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.app.Activity; -import android.app.AlertDialog; import android.app.Fragment; import android.content.Intent; import android.content.res.Configuration; @@ -39,15 +38,13 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountByID; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; -import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; -import org.joinmastodon.android.api.requests.accounts.SetAccountMuted; import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.Relationship; -import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable; import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayoutMediator; @@ -382,9 +379,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList followersCount.setText(UiUtils.abbreviateNumber(account.followersCount)); followingCount.setText(UiUtils.abbreviateNumber(account.followingCount)); postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount)); - followersLabel.setText(getResources().getQuantityString(R.plurals.followers, account.followersCount)); - followingLabel.setText(getResources().getQuantityString(R.plurals.following, account.followingCount)); - postsLabel.setText(getResources().getQuantityString(R.plurals.posts, account.statusesCount)); + followersLabel.setText(getResources().getQuantityString(R.plurals.followers, Math.min(999, account.followersCount))); + followingLabel.setText(getResources().getQuantityString(R.plurals.following, Math.min(999, account.followingCount))); + postsLabel.setText(getResources().getQuantityString(R.plurals.posts, Math.min(999, account.statusesCount))); UiUtils.loadCustomEmojiInTextView(name); UiUtils.loadCustomEmojiInTextView(bio); @@ -508,13 +505,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private void updateRelationship(){ invalidateOptionsMenu(); actionButton.setVisibility(View.VISIBLE); - if(relationship.blocking){ - actionButton.setText(R.string.button_blocked); - }else if(relationship.muting){ - actionButton.setText(R.string.button_muted); - }else{ - actionButton.setText(relationship.following ? R.string.button_following : R.string.button_follow); - } + UiUtils.setRelationshipToActionButton(relationship, actionButton); } private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){ @@ -569,13 +560,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList else saveAndExitEditMode(); }else{ - if(relationship.blocking){ - confirmToggleBlocked(); - }else if(relationship.muting){ - confirmToggleMuted(); - }else{ - toggleFollowing(); - } + UiUtils.performAccountAction(getActivity(), account, accountID, relationship, actionButton, this::setActionProgressVisible, this::updateRelationship); } } @@ -712,26 +697,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList .exec(accountID); } - private void toggleFollowing(){ - setActionProgressVisible(true); - new SetAccountFollowed(account.id, !relationship.following) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(Relationship result){ - relationship=result; - updateRelationship(); - setActionProgressVisible(false); - } - - @Override - public void onError(ErrorResponse error){ - error.showToast(getActivity()); - setActionProgressVisible(false); - } - }) - .exec(accountID); - } - private void confirmToggleMuted(){ UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship); } @@ -829,10 +794,4 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList return position; } } - - private class SimpleViewHolder extends RecyclerView.ViewHolder{ - public SimpleViewHolder(@NonNull View itemView){ - super(itemView); - } - } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/TrendingHashtagsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/TrendingHashtagsFragment.java new file mode 100644 index 00000000..a20a82c6 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/TrendingHashtagsFragment.java @@ -0,0 +1,107 @@ +package org.joinmastodon.android.fragments; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags; +import org.joinmastodon.android.model.Hashtag; +import org.joinmastodon.android.ui.DividerItemDecoration; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.HashtagChartView; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.fragments.BaseRecyclerFragment; +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; +import me.grishka.appkit.views.UsableRecyclerView; + +public class TrendingHashtagsFragment extends BaseRecyclerFragment{ + private String accountID; + + public TrendingHashtagsFragment(){ + super(10); + } + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + accountID=getArguments().getString("account"); + } + + @Override + protected void doLoadData(int offset, int count){ + currentRequest=new GetTrendingHashtags(10) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(List result){ + onDataLoaded(result, false); + } + }) + .exec(accountID); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + return new HashtagsAdapter(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16)); + } + + private class HashtagsAdapter extends RecyclerView.Adapter{ + @NonNull + @Override + public HashtagViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new HashtagViewHolder(); + } + + @Override + public void onBindViewHolder(@NonNull HashtagViewHolder holder, int position){ + holder.bind(data.get(position)); + } + + @Override + public int getItemCount(){ + return data.size(); + } + } + + private class HashtagViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ + private final TextView title, subtitle; + private final HashtagChartView chart; + + public HashtagViewHolder(){ + super(getActivity(), R.layout.item_trending_hashtag, list); + title=findViewById(R.id.title); + subtitle=findViewById(R.id.subtitle); + chart=findViewById(R.id.chart); + } + + @Override + public void onBind(Hashtag item){ + title.setText('#'+item.name); + int numPeople=item.history.get(0).accounts; + if(item.history.size()>1) + numPeople+=item.history.get(1).accounts; + subtitle.setText(getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople)); + chart.setData(item.history); + } + + @Override + public void onClick(){ + + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java index a4423aa9..4e04f2db 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java @@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.os.LocaleList; @@ -22,7 +21,7 @@ import android.widget.Toast; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonErrorResponse; -import org.joinmastodon.android.api.requests.GetInstance; +import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories; import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances; import org.joinmastodon.android.api.session.AccountSessionManager; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Card.java b/mastodon/src/main/java/org/joinmastodon/android/model/Card.java index 56260340..00ea52c9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Card.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Card.java @@ -11,6 +11,8 @@ import org.joinmastodon.android.ui.utils.BlurHashDecoder; import org.joinmastodon.android.ui.utils.BlurHashDrawable; import org.parceler.Parcel; +import java.util.List; + @Parcel public class Card extends BaseModel{ @RequiredField @@ -31,6 +33,7 @@ public class Card extends BaseModel{ public String image; public String embedUrl; public String blurhash; + public List history; public transient Drawable blurhashPlaceholder; @@ -60,6 +63,7 @@ public class Card extends BaseModel{ ", image='"+image+'\''+ ", embedUrl='"+embedUrl+'\''+ ", blurhash='"+blurhash+'\''+ + ", history="+history+ '}'; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/FollowSuggestion.java b/mastodon/src/main/java/org/joinmastodon/android/model/FollowSuggestion.java new file mode 100644 index 00000000..ca5f55cd --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/FollowSuggestion.java @@ -0,0 +1,16 @@ +package org.joinmastodon.android.model; + +import org.joinmastodon.android.api.ObjectValidationException; +import org.joinmastodon.android.api.RequiredField; + +public class FollowSuggestion extends BaseModel{ + @RequiredField + public Account account; +// public String source; + + @Override + public void postprocess() throws ObjectValidationException{ + super.postprocess(); + account.postprocess(); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/DividerItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/DividerItemDecoration.java new file mode 100644 index 00000000..2a53702f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/DividerItemDecoration.java @@ -0,0 +1,43 @@ +package org.joinmastodon.android.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.View; + +import org.joinmastodon.android.ui.utils.UiUtils; + +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.utils.V; + +public class DividerItemDecoration extends RecyclerView.ItemDecoration{ + private Paint paint=new Paint(); + private int paddingStart, paddingEnd; + + public DividerItemDecoration(Context context, @AttrRes int color, float thicknessDp, int paddingStartDp, int paddingEndDp){ + paint.setColor(UiUtils.getThemeColor(context, color)); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(V.dp(thicknessDp)); + paddingStart=V.dp(paddingStartDp); + paddingEnd=V.dp(paddingEndDp); + } + + @Override + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + boolean isRTL=parent.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL; + int padLeft=isRTL ? paddingEnd : paddingStart; + int padRight=isRTL ? paddingStart : paddingEnd; + int totalItems=parent.getAdapter().getItemCount(); + for(int i=0;i roundedRects=new SparseArray<>(); + private OutlineProviders(){ //no instance } @@ -16,4 +21,26 @@ public class OutlineProviders{ outline.setAlpha(view.getAlpha()); } }; + + public static ViewOutlineProvider roundedRect(int dp){ + ViewOutlineProvider provider=roundedRects.get(dp); + if(provider!=null) + return provider; + provider=new RoundRectOutlineProvider(V.dp(dp)); + roundedRects.put(dp, provider); + return provider; + } + + private static class RoundRectOutlineProvider extends ViewOutlineProvider{ + private final int radius; + + private RoundRectOutlineProvider(int radius){ + this.radius=radius; + } + + @Override + public void getOutline(View view, Outline outline){ + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/SimpleViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/SimpleViewHolder.java new file mode 100644 index 00000000..11ddb0ea --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/SimpleViewHolder.java @@ -0,0 +1,12 @@ +package org.joinmastodon.android.ui; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public class SimpleViewHolder extends RecyclerView.ViewHolder{ + public SimpleViewHolder(@NonNull View itemView){ + super(itemView); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index 2cfcec5f..c83f28fa 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -1,7 +1,6 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; -import android.app.Fragment; import android.graphics.Outline; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; @@ -24,6 +23,7 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; @@ -50,7 +50,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ super(parentID, parentFragment); this.user=user; this.createdAt=createdAt; - avaRequest=new UrlImageLoaderRequest(user.avatar); + avaRequest=new UrlImageLoaderRequest(user.avatar, V.dp(50), V.dp(50)); this.accountID=accountID; parsedName=new SpannableStringBuilder(user.displayName); this.status=status; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java index 06eb4097..af9f6c90 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java @@ -3,7 +3,6 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; -import android.util.StateSet; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; @@ -12,6 +11,7 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import java.util.Locale; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java index 9f28c28b..bf821a06 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java @@ -11,6 +11,7 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.UiUtils; import java.util.List; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index 239a7f61..2568362c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -11,6 +11,7 @@ import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.views.LinkedTextView; import me.grishka.appkit.imageloader.ImageLoaderViewHolder; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java index 72a8bf70..33906813 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java @@ -2325,8 +2325,8 @@ public class TabLayout extends HorizontalScrollView { /** A {@link LinearLayout} containing {@link Tab} instances for use with {@link TabLayout}. */ public final class TabView extends LinearLayout { private Tab tab; - private TextView textView; - private ImageView iconView; + public TextView textView; + public ImageView iconView; @Nullable private View badgeAnchorView; // @Nullable private BadgeDrawable badgeDrawable; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/CustomEmojiHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/CustomEmojiHelper.java similarity index 93% rename from mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/CustomEmojiHelper.java rename to mastodon/src/main/java/org/joinmastodon/android/ui/utils/CustomEmojiHelper.java index a9efc97e..a9a20892 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/CustomEmojiHelper.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/CustomEmojiHelper.java @@ -1,4 +1,4 @@ -package org.joinmastodon.android.ui.displayitems; +package org.joinmastodon.android.ui.utils; import android.graphics.drawable.Drawable; import android.text.Spanned; @@ -12,7 +12,7 @@ import java.util.stream.Collectors; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; -class CustomEmojiHelper{ +public class CustomEmojiHelper{ public List> spans=new ArrayList<>(); public List requests=new ArrayList<>(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index f9c26425..a2092580 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -13,12 +13,14 @@ import android.os.Looper; import android.provider.OpenableColumns; import android.text.Spanned; import android.view.View; +import android.widget.Button; import android.widget.TextView; import org.joinmastodon.android.E; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; +import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountMuted; import org.joinmastodon.android.api.requests.statuses.DeleteStatus; import org.joinmastodon.android.events.StatusDeletedEvent; @@ -200,7 +202,7 @@ public class UiUtils{ public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer resultCallback){ showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title), activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName), - activity.getString(currentlyBlocked ? R.string.do_block : R.string.do_unblock), ()->{ + activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{ new SetAccountBlocked(account.id, !currentlyBlocked) .setCallback(new Callback<>(){ @Override @@ -258,4 +260,39 @@ public class UiUtils{ .exec(accountID); }); } + + public static void setRelationshipToActionButton(Relationship relationship, Button button){ + if(relationship.blocking){ + button.setText(R.string.button_blocked); + }else if(relationship.muting){ + button.setText(R.string.button_muted); + }else{ + button.setText(relationship.following ? R.string.button_following : R.string.button_follow); + } + } + + public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer progressCallback, Consumer resultCallback){ + if(relationship.blocking){ + confirmToggleBlockUser(activity, accountID, account, true, resultCallback); + }else if(relationship.muting){ + confirmToggleMuteUser(activity, accountID, account, true, resultCallback); + }else{ + progressCallback.accept(true); + new SetAccountFollowed(account.id, !relationship.following) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Relationship result){ + resultCallback.accept(result); + progressCallback.accept(false); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(activity); + progressCallback.accept(false); + } + }) + .exec(accountID); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/HashtagChartView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HashtagChartView.java new file mode 100644 index 00000000..19caf052 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HashtagChartView.java @@ -0,0 +1,94 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.CornerPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.View; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.model.History; +import org.joinmastodon.android.ui.utils.UiUtils; + +import java.util.List; + +import me.grishka.appkit.utils.V; + +public class HashtagChartView extends View{ + 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 float[] relativeOffsets=new float[7]; + + public HashtagChartView(Context context){ + this(context, null); + } + + public HashtagChartView(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public HashtagChartView(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + paint.setStrokeWidth(V.dp(1.71f)); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeJoin(Paint.Join.ROUND); + } + + public void setData(List data){ + int max=0; + for(History h:data){ + max=Math.max(h.accounts, max); + } + if(relativeOffsets.length!=data.size()) + relativeOffsets=new float[data.size()]; + int i=0; + for(History h:data){ + relativeOffsets[i]=(float)h.accounts/max; + i++; + } + updatePath(); + } + + private void updatePath(){ + if(getWidth()<1) + 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)); + for(int i=1;i + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_discover_tabs.xml b/mastodon/src/main/res/drawable/bg_discover_tabs.xml new file mode 100644 index 00000000..245179f2 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_discover_tabs.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_search_field.xml b/mastodon/src/main/res/drawable/bg_search_field.xml new file mode 100644 index 00000000..1b9ae5eb --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_search_field.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/discover_ava_bg.xml b/mastodon/src/main/res/drawable/discover_ava_bg.xml new file mode 100644 index 00000000..61f19b4f --- /dev/null +++ b/mastodon/src/main/res/drawable/discover_ava_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_fluent_search_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_search_24_regular.xml new file mode 100644 index 00000000..d2a649a2 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_search_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/fragment_discover.xml b/mastodon/src/main/res/layout/fragment_discover.xml new file mode 100644 index 00000000..2015ed4a --- /dev/null +++ b/mastodon/src/main/res/layout/fragment_discover.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_discover_account.xml b/mastodon/src/main/res/layout/item_discover_account.xml new file mode 100644 index 00000000..ba98da6f --- /dev/null +++ b/mastodon/src/main/res/layout/item_discover_account.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_trending_hashtag.xml b/mastodon/src/main/res/layout/item_trending_hashtag.xml new file mode 100644 index 00000000..235f591d --- /dev/null +++ b/mastodon/src/main/res/layout/item_trending_hashtag.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_trending_link.xml b/mastodon/src/main/res/layout/item_trending_link.xml new file mode 100644 index 00000000..219dae1a --- /dev/null +++ b/mastodon/src/main/res/layout/item_trending_link.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml index 9c490673..8891ac38 100644 --- a/mastodon/src/main/res/values/attrs.xml +++ b/mastodon/src/main/res/values/attrs.xml @@ -11,6 +11,10 @@ + + + + diff --git a/mastodon/src/main/res/values/ids.xml b/mastodon/src/main/res/values/ids.xml index 9770f9ba..ff44c719 100644 --- a/mastodon/src/main/res/values/ids.xml +++ b/mastodon/src/main/res/values/ids.xml @@ -6,4 +6,9 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index b35cbbd5..a1cb04b1 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -133,4 +133,16 @@ Pause Log out Add account + Search + Hashtags + News + For you + + %d person is talking + %d people are talking + + + Discussed %d time + Discussed %d times + \ No newline at end of file diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index e762dc77..b33bf5a3 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -28,6 +28,10 @@ @color/primary_500 @color/gray_300 @color/primary_600 + @color/gray_200 + @color/gray_600 + @color/gray_400 + @color/primary_100 @drawable/bg_button_primary_dark_on_light @@ -64,6 +68,12 @@ @color/gray_600 @color/primary_600 + + @color/gray_200 + @color/gray_600 + @color/gray_400 + @color/primary_100 + @drawable/bg_button_primary_light_on_dark false @@ -172,6 +182,7 @@