From e91a2f7984d1a01ff7397e3935a0859fce9e0976 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 25 Dec 2022 17:56:31 +0100 Subject: [PATCH] Fix issue #672 - Support pagination for search / trending --- .../client/entities/api/Pagination.java | 1 + .../android/client/entities/api/Results.java | 1 + .../android/helper/MastodonHelper.java | 1 + .../timeline/FragmentMastodonAccount.java | 48 +++++++++---- .../timeline/FragmentMastodonTag.java | 68 +++++++++++++++++-- .../timeline/FragmentMastodonTimeline.java | 50 ++++++++++---- .../android/viewmodel/mastodon/SearchVM.java | 8 ++- .../viewmodel/mastodon/TimelinesVM.java | 4 +- .../metadata/android/en/changelogs/453.txt | 5 ++ 9 files changed, 151 insertions(+), 35 deletions(-) create mode 100644 src/fdroid/fastlane/metadata/android/en/changelogs/453.txt diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Pagination.java b/app/src/main/java/app/fedilab/android/client/entities/api/Pagination.java index 1b024270e..3e36371a9 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Pagination.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Pagination.java @@ -19,4 +19,5 @@ public class Pagination { public String max_id; public String min_id; public String since_id; + public Integer offset; } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Results.java b/app/src/main/java/app/fedilab/android/client/entities/api/Results.java index 9e6b387ea..60fc77d91 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Results.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Results.java @@ -24,5 +24,6 @@ public class Results { public java.util.List statuses; @SerializedName("hashtags") public java.util.List hashtags; + public Pagination pagination; } diff --git a/app/src/main/java/app/fedilab/android/helper/MastodonHelper.java b/app/src/main/java/app/fedilab/android/helper/MastodonHelper.java index 954c38ff1..b86a9ecd7 100644 --- a/app/src/main/java/app/fedilab/android/helper/MastodonHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/MastodonHelper.java @@ -80,6 +80,7 @@ public class MastodonHelper { public static final int ACCOUNTS_PER_CALL = 40; public static final int STATUSES_PER_CALL = 40; + public static final int SEARCH_PER_CALL = 20; public static final int NOTIFICATIONS_PER_CALL = 30; diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java index afef61683..7c7289b75 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java @@ -56,6 +56,7 @@ public class FragmentMastodonAccount extends Fragment { private boolean flagLoading; private List accounts; private String max_id; + private Integer offset; private AccountAdapter accountAdapter; private String search; private Account accountTimeline; @@ -84,6 +85,7 @@ public class FragmentMastodonAccount extends Fragment { binding.recyclerView.setVisibility(View.GONE); accountsVM = new ViewModelProvider(FragmentMastodonAccount.this).get(viewModelKey, AccountsVM.class); max_id = null; + offset = 0; router(true); } @@ -109,18 +111,31 @@ public class FragmentMastodonAccount extends Fragment { } } else if (search != null) { SearchVM searchVM = new ViewModelProvider(FragmentMastodonAccount.this).get(viewModelKey, SearchVM.class); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL) - .observe(getViewLifecycleOwner(), results -> { - if (results != null) { - Accounts accounts = new Accounts(); - Pagination pagination = new Pagination(); - accounts.accounts = results.accounts; - accounts.pagination = pagination; - initializeAccountCommonView(accounts); - } else { - Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); - } - }); + if (firstLoad) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, 0, null, null, MastodonHelper.SEARCH_PER_CALL) + .observe(getViewLifecycleOwner(), results -> { + if (results != null) { + Accounts accounts = new Accounts(); + Pagination pagination = new Pagination(); + accounts.accounts = results.accounts; + accounts.pagination = pagination; + initializeAccountCommonView(accounts); + } else { + Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, offset, null, null, MastodonHelper.SEARCH_PER_CALL) + .observe(getViewLifecycleOwner(), results -> { + if (results != null) { + Accounts accounts = new Accounts(); + Pagination pagination = new Pagination(); + accounts.accounts = results.accounts; + accounts.pagination = pagination; + dealWithPagination(accounts); + } + }); + } } else if (timelineType == Timeline.TimeLineEnum.MUTED_TIMELINE) { if (firstLoad) { accountsVM.getMutes(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.accountsPerCall(requireActivity())), null, null) @@ -204,7 +219,11 @@ public class FragmentMastodonAccount extends Fragment { this.accounts = accounts.accounts; accountAdapter = new AccountAdapter(this.accounts, timelineType == Timeline.TimeLineEnum.MUTED_TIMELINE_HOME); - flagLoading = accounts.pagination.max_id == null; + if (search == null) { + flagLoading = accounts.pagination.max_id == null; + } else { + offset += MastodonHelper.SEARCH_PER_CALL; + } LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(accountAdapter); @@ -263,6 +282,9 @@ public class FragmentMastodonAccount extends Fragment { //Fetch the relationship fetchRelationShip(fetched_accounts.accounts, position); max_id = fetched_accounts.pagination.max_id; + if (search != null) { + offset += MastodonHelper.SEARCH_PER_CALL; + } accountAdapter.notifyItemRangeInserted(startId, fetched_accounts.accounts.size()); } else { flagLoading = true; 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 45f9d33d8..b6bf6b18e 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 @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.Collections; @@ -49,6 +50,9 @@ public class FragmentMastodonTag extends Fragment { private TagAdapter tagAdapter; private String search; private Timeline.TimeLineEnum timelineType; + private Integer offset; + private boolean flagLoading; + private List tagList; public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -66,6 +70,10 @@ public class FragmentMastodonTag extends Fragment { super.onViewCreated(view, savedInstanceState); binding.loader.setVisibility(View.VISIBLE); binding.recyclerView.setVisibility(View.GONE); + offset = 0; + flagLoading = false; + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setEnabled(false); router(); } @@ -75,16 +83,24 @@ public class FragmentMastodonTag extends Fragment { private void router() { 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) + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "hashtags", false, true, false, offset, null, null, MastodonHelper.SEARCH_PER_CALL) .observe(getViewLifecycleOwner(), results -> { - if (results != null && results.hashtags != null) { + if (results != null && results.hashtags != null && offset == 0) { initializeTagCommonView(results.hashtags); + } else if (results != null && results.hashtags != null) { + dealWithPaginationTag(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(), this::initializeTagCommonView); + timelinesVM.getTagsTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, offset, MastodonHelper.SEARCH_PER_CALL) + .observe(getViewLifecycleOwner(), tags -> { + if (tags != null && offset == 0) { + initializeTagCommonView(tags); + } else if (tags != null) { + dealWithPaginationTag(tags); + } + }); } } @@ -92,6 +108,24 @@ public class FragmentMastodonTag extends Fragment { binding.recyclerView.setAdapter(tagAdapter); } + private void dealWithPaginationTag(final List tags) { + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + if (tags == null || tags.size() == 0) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.GONE); + return; + } + offset += MastodonHelper.SEARCH_PER_CALL; + binding.swipeContainer.setRefreshing(false); + binding.loadingNextElements.setVisibility(View.GONE); + flagLoading = false; + int start = tagList.size(); + tagList.addAll(tags); + tagAdapter.notifyItemRangeInserted(start, tags.size()); + } + /** * Intialize the view for tags * @@ -101,6 +135,7 @@ public class FragmentMastodonTag extends Fragment { if (binding == null || !isAdded() || getActivity() == null) { return; } + tagList = new ArrayList<>(); binding.loader.setVisibility(View.GONE); binding.noAction.setVisibility(View.GONE); binding.swipeContainer.setRefreshing(false); @@ -130,12 +165,35 @@ public class FragmentMastodonTag extends Fragment { tags.add(0, tag); } } + offset += MastodonHelper.SEARCH_PER_CALL; binding.recyclerView.setVisibility(View.VISIBLE); binding.noAction.setVisibility(View.GONE); - tagAdapter = new TagAdapter(tags); + tagList.addAll(tags); + tagAdapter = new TagAdapter(tagList); LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(tagAdapter); + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + } + }); } } \ No newline at end of file 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 cdc3462a4..0256564a4 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 @@ -80,6 +80,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. private String search, searchCache; private Status statusReport; private String max_id, min_id, min_id_fetch_more, max_id_fetch_more; + private Integer offset; private StatusAdapter statusAdapter; private Timeline.TimeLineEnum timelineType; private List timelineStatuses; @@ -188,6 +189,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. if (statusAdapter != null) { statusAdapter.notifyItemRangeRemoved(0, count); max_id = statusReport != null ? statusReport.id : null; + offset = 0; SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); rememberPosition = sharedpreferences.getBoolean(getString(R.string.SET_REMEMBER_POSITION), true); //Inner marker are only for pinned timelines and main timelines, they have isViewInitialized set to false @@ -281,6 +283,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. binding.loader.setVisibility(View.VISIBLE); binding.recyclerView.setVisibility(View.GONE); max_id = statusReport != null ? statusReport.id : null; + offset = 0; SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); rememberPosition = sharedpreferences.getBoolean(getString(R.string.SET_REMEMBER_POSITION), true); //Inner marker are only for pinned timelines and main timelines, they have isViewInitialized set to false @@ -428,7 +431,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } //Update the timeline with new statuses int insertedStatus; - if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE_PUBLIC && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { + if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE_PUBLIC && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE && search == null) { insertedStatus = updateStatusListWith(fetched_statuses.statuses); } else { //Trends cannot be ordered by id insertedStatus = fetched_statuses.statuses.size(); @@ -455,6 +458,9 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. min_id = fetched_statuses.pagination.min_id; } } + if (search != null) { + offset += MastodonHelper.SEARCH_PER_CALL; + } int sizeBeforeFilter = 0; int filteredMessage = 0; int requestedMessages = MastodonHelper.statusesPerCall(requireActivity()); @@ -557,6 +563,9 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. if (min_id == null || (statuses.pagination.min_id != null && Helper.compareTo(statuses.pagination.min_id, min_id) > 0)) { min_id = statuses.pagination.min_id; } + if (search != null) { + offset += MastodonHelper.SEARCH_PER_CALL; + } statusAdapter = new StatusAdapter(timelineStatuses, timelineType, minified, canBeFederated, checkRemotely); statusAdapter.fetchMoreCallBack = this; if (statusReport != null) { @@ -570,8 +579,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(statusAdapter); - - if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { + if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { @@ -1033,17 +1041,31 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } } else if (search != null) { SearchVM searchVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, SearchVM.class); - searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, null, false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL) - .observe(getViewLifecycleOwner(), results -> { - if (results != null) { - Statuses statuses = new Statuses(); - statuses.statuses = results.statuses; - statuses.pagination = new Pagination(); - initializeStatusesCommonView(statuses); - } else { - Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_LONG).show(); - } - }); + if (direction == null) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, null, false, true, false, 0, null, null, MastodonHelper.SEARCH_PER_CALL) + .observe(getViewLifecycleOwner(), results -> { + if (results != null) { + Statuses statuses = new Statuses(); + statuses.statuses = results.statuses; + statuses.pagination = new Pagination(); + initializeStatusesCommonView(statuses); + } else { + Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_LONG).show(); + } + }); + } else if (direction == DIRECTION.BOTTOM) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, null, false, true, false, offset, null, null, MastodonHelper.SEARCH_PER_CALL) + .observe(getViewLifecycleOwner(), results -> { + if (results != null) { + Statuses statuses = new Statuses(); + statuses.statuses = results.statuses; + statuses.pagination = new Pagination(); + dealWithPagination(statuses, direction, false); + } + }); + } else { + flagLoading = false; + } } else if (searchCache != null) { SearchVM searchVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, SearchVM.class); searchVM.searchCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, searchCache.trim()) diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java index e52c973b2..23bb6d0b4 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java @@ -99,10 +99,15 @@ public class SearchVM extends AndroidViewModel { MastodonSearchService mastodonSearchService = init(instance); resultsMutableLiveData = new MutableLiveData<>(); new Thread(() -> { + int finalLimit = 40; + if (limit != null && limit < 40) { + finalLimit = limit; + } Call resultsCall = mastodonSearchService.search( token, q, account_id, type, exclude_unreviewed, - resolve, following, offset, max_id, min_id, limit); + resolve, following, offset, max_id, min_id, finalLimit); Results results = null; + if (resultsCall != null) { try { Response resultsResponse = resultsCall.execute(); @@ -118,6 +123,7 @@ public class SearchVM extends AndroidViewModel { if (results.hashtags == null) { results.hashtags = new ArrayList<>(); } + results.pagination.offset = finalLimit; } } } catch (Exception e) { 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 a40ddaa13..ec3eccaaf 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 @@ -198,11 +198,11 @@ public class TimelinesVM extends AndroidViewModel { return statusesMutableLiveData; } - public LiveData> getTagsTrends(String token, @NonNull String instance) { + public LiveData> getTagsTrends(String token, @NonNull String instance, Integer offset, Integer limit) { MastodonTimelinesService mastodonTimelinesService = init(instance); tagListMutableLiveData = new MutableLiveData<>(); new Thread(() -> { - Call> publicTlCall = mastodonTimelinesService.getTagTrends(token); + Call> publicTlCall = mastodonTimelinesService.getTagTrends(token, offset, limit); List tagList = null; if (publicTlCall != null) { try { diff --git a/src/fdroid/fastlane/metadata/android/en/changelogs/453.txt b/src/fdroid/fastlane/metadata/android/en/changelogs/453.txt new file mode 100644 index 000000000..8b283774c --- /dev/null +++ b/src/fdroid/fastlane/metadata/android/en/changelogs/453.txt @@ -0,0 +1,5 @@ +Fixed: +- Long press on Nitter tabs +- Open with another accounts +- Chars size not respected for Android 5-6 +- Wrong instance fetched for instances.social \ No newline at end of file