From a80bdd6ac3a9f36250dc1d52e27f3b1e42db614e Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 9 Jul 2022 17:34:08 +0200 Subject: [PATCH] Add trends --- app/src/main/AndroidManifest.xml | 5 + .../app/fedilab/android/BaseMainActivity.java | 4 + .../android/activities/TrendsActivity.java | 156 ++++++++++++++++++ .../endpoints/MastodonTimelinesService.java | 9 + .../android/client/entities/app/Timeline.java | 4 + .../timeline/FragmentMastodonTag.java | 12 +- .../timeline/FragmentMastodonTimeline.java | 27 ++- .../viewmodel/mastodon/TimelinesVM.java | 53 ++++++ .../drawable/ic_baseline_trending_up_24.xml | 11 ++ app/src/main/res/layout/activity_trends.xml | 38 +++++ .../main/res/menu/activity_main_drawer.xml | 6 + 11 files changed, 318 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/app/fedilab/android/activities/TrendsActivity.java create mode 100644 app/src/main/res/drawable/ic_baseline_trending_up_24.xml create mode 100644 app/src/main/res/layout/activity_trends.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3c0f0dc06..481ca2625 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -117,6 +117,11 @@ android:configChanges="keyboardHidden|orientation|screenSize" android:theme="@style/AppThemeBar" android:label="@string/search" /> + . */ + +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.tabs.TabLayout; + +import org.jetbrains.annotations.NotNull; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.app.Timeline; +import app.fedilab.android.databinding.ActivityTrendsBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTag; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; + + +public class TrendsActivity extends BaseActivity { + + + private ActivityTrendsBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityTrendsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); + } + + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots))); + binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(TrendsActivity.this, R.attr.mTextColor), ContextCompat.getColor(TrendsActivity.this, R.color.cyanea_accent_dark_reference)); + binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(TrendsActivity.this)); + binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.trendsViewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + Fragment fragment; + if (binding.trendsViewpager.getAdapter() != null) { + fragment = (Fragment) binding.trendsViewpager.getAdapter().instantiateItem(binding.trendsViewpager, tab.getPosition()); + if (fragment instanceof FragmentMastodonTimeline) { + FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); + fragmentMastodonTimeline.scrollToTop(); + } else if (fragment instanceof FragmentMastodonTag) { + FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment); + fragmentMastodonTag.scrollToTop(); + } + } + } + }); + + PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + binding.trendsViewpager.setAdapter(mPagerAdapter); + binding.trendsViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position); + if (tab != null) + tab.select(); + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }); + } + + + @Override + public boolean onOptionsItemSelected(@NotNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + + /** + * Pager adapter for the 4 fragments + */ + @SuppressWarnings("deprecation") + private static class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @NotNull + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + if (position == 0) { + FragmentMastodonTag fragmentMastodonTag = new FragmentMastodonTag(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_TAG); + fragmentMastodonTag.setArguments(bundle); + return fragmentMastodonTag; + } + FragmentMastodonTimeline fragmentMastodonTimeline = new FragmentMastodonTimeline(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_MESSAGE); + fragmentMastodonTimeline.setArguments(bundle); + return fragmentMastodonTimeline; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + } + + @Override + public int getCount() { + return 2; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java index 36d8c9f65..09d6da1c7 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java @@ -21,6 +21,7 @@ import app.fedilab.android.client.entities.api.Conversation; import app.fedilab.android.client.entities.api.Marker; import app.fedilab.android.client.entities.api.MastodonList; import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.misskey.MisskeyNote; import app.fedilab.android.client.entities.nitter.Nitter; import app.fedilab.android.client.entities.peertube.PeertubeVideo; @@ -52,6 +53,14 @@ public interface MastodonTimelinesService { @Query("limit") Integer limit ); + + @GET("trends/statuses") + Call> getStatusTrends(@Header("Authorization") String token); + + + @GET("trends/tags") + Call> getTagTrends(@Header("Authorization") String token); + //Public Tags timelines @GET("timelines/tag/{hashtag}") Call> getHashTag( diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java index b8a4846c0..d26badc9e 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java @@ -370,6 +370,10 @@ public class Timeline { LIST("LIST"), @SerializedName("REMOTE") REMOTE("REMOTE"), + @SerializedName("TREND_TAG") + TREND_TAG("TREND_TAG"), + @SerializedName("TREND_MESSAGE") + TREND_MESSAGE("TREND_MESSAGE"), @SerializedName("ACCOUNT_TIMELINE") ACCOUNT_TIMELINE("ACCOUNT_TIMELINE"), @SerializedName("MUTED_TIMELINE") diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java index eca471846..e287dd40e 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java @@ -31,12 +31,14 @@ import java.util.List; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; import app.fedilab.android.client.entities.api.Tag; +import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.FragmentPaginationBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.drawer.TagAdapter; import app.fedilab.android.viewmodel.mastodon.SearchVM; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; public class FragmentMastodonTag extends Fragment { @@ -45,11 +47,13 @@ public class FragmentMastodonTag extends Fragment { private FragmentPaginationBinding binding; private TagAdapter tagAdapter; private String search; + private Timeline.TimeLineEnum timelineType; public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (getArguments() != null) { search = getArguments().getString(Helper.ARG_SEARCH_KEYWORD, null); + timelineType = (Timeline.TimeLineEnum) getArguments().get(Helper.ARG_TIMELINE_TYPE); } binding = FragmentPaginationBinding.inflate(inflater, container, false); @@ -74,7 +78,7 @@ public class FragmentMastodonTag extends Fragment { * Router for timelines */ private void router() { - if (search != null) { + if (search != null && timelineType == null) { SearchVM searchVM = new ViewModelProvider(FragmentMastodonTag.this).get(SearchVM.class); searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "hashtags", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL) .observe(getViewLifecycleOwner(), results -> { @@ -82,6 +86,12 @@ public class FragmentMastodonTag extends Fragment { initializeTagCommonView(results.hashtags); } }); + } else if (timelineType == Timeline.TimeLineEnum.TREND_TAG) { + TimelinesVM timelinesVM = new ViewModelProvider(FragmentMastodonTag.this).get(TimelinesVM.class); + timelinesVM.getTagsTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance) + .observe(getViewLifecycleOwner(), tags -> { + initializeTagCommonView(tags); + }); } } diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java index 549124093..5d3089797 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java @@ -286,11 +286,13 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. binding.loader.setVisibility(View.GONE); binding.noAction.setVisibility(View.GONE); binding.swipeContainer.setRefreshing(false); - binding.swipeContainer.setOnRefreshListener(() -> { - binding.swipeContainer.setRefreshing(true); - flagLoading = false; - route(DIRECTION.REFRESH, true); - }); + if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + route(DIRECTION.REFRESH, true); + }); + } if (statuses == null || statuses.statuses == null || statuses.statuses.size() == 0) { binding.noAction.setVisibility(View.VISIBLE); @@ -356,7 +358,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. binding.recyclerView.scrollToPosition(position); } - if (searchCache == null) { + if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { @@ -849,6 +851,19 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else { flagLoading = false; } + } else if (timelineType == Timeline.TimeLineEnum.TREND_MESSAGE) { + if (direction == null) { + timelinesVM.getStatusTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance) + .observe(getViewLifecycleOwner(), statusesTrends -> { + Statuses statuses = new Statuses(); + statuses.statuses = new ArrayList<>(); + if (statusesTrends != null) { + statuses.statuses.addAll(statusesTrends); + } + statuses.pagination = new Pagination(); + initializeStatusesCommonView(statuses); + }); + } } }; mainHandler.post(myRunnable); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java index 4ed5ad2cc..a04c2a0de 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java @@ -38,6 +38,7 @@ import app.fedilab.android.client.entities.api.MastodonList; import app.fedilab.android.client.entities.api.Pagination; import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.client.entities.api.Statuses; +import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.app.BaseAccount; import app.fedilab.android.client.entities.app.StatusCache; import app.fedilab.android.client.entities.app.StatusDraft; @@ -75,6 +76,8 @@ public class TimelinesVM extends AndroidViewModel { private MutableLiveData mastodonListMutableLiveData; private MutableLiveData> mastodonListListMutableLiveData; private MutableLiveData markerMutableLiveData; + private MutableLiveData> statusListMutableLiveData; + private MutableLiveData> tagListMutableLiveData; public TimelinesVM(@NonNull Application application) { super(application); @@ -107,6 +110,56 @@ public class TimelinesVM extends AndroidViewModel { return retrofit.create(MastodonTimelinesService.class); } + public LiveData> getStatusTrends(String token, @NonNull String instance) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + statusListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call> publicTlCall = mastodonTimelinesService.getStatusTrends(token); + List statusList = null; + if (publicTlCall != null) { + try { + Response> publicTlResponse = publicTlCall.execute(); + if (publicTlResponse.isSuccessful()) { + statusList = publicTlResponse.body(); + statusList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalStatusList = statusList; + Runnable myRunnable = () -> statusListMutableLiveData.setValue(finalStatusList); + mainHandler.post(myRunnable); + }).start(); + return statusListMutableLiveData; + } + + + public LiveData> getTagsTrends(String token, @NonNull String instance) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + tagListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call> publicTlCall = mastodonTimelinesService.getTagTrends(token); + List tagList = null; + if (publicTlCall != null) { + try { + Response> publicTlResponse = publicTlCall.execute(); + if (publicTlResponse.isSuccessful()) { + tagList = publicTlResponse.body(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalTagList = tagList; + Runnable myRunnable = () -> tagListMutableLiveData.setValue(finalTagList); + mainHandler.post(myRunnable); + }).start(); + return tagListMutableLiveData; + } + /** * Public timeline * diff --git a/app/src/main/res/drawable/ic_baseline_trending_up_24.xml b/app/src/main/res/drawable/ic_baseline_trending_up_24.xml new file mode 100644 index 000000000..04a5a8631 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_trending_up_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/activity_trends.xml b/app/src/main/res/layout/activity_trends.xml new file mode 100644 index 000000000..75ea988a0 --- /dev/null +++ b/app/src/main/res/layout/activity_trends.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index cd0a799fd..4ab3fc405 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -35,6 +35,12 @@ android:title="@string/action_announcements" android:visible="true" /> + +