Fix issue #672 - Support pagination for search / trending

This commit is contained in:
Thomas 2022-12-25 17:56:31 +01:00
parent f38ae254c1
commit e91a2f7984
9 changed files with 151 additions and 35 deletions

View File

@ -19,4 +19,5 @@ public class Pagination {
public String max_id;
public String min_id;
public String since_id;
public Integer offset;
}

View File

@ -24,5 +24,6 @@ public class Results {
public java.util.List<Status> statuses;
@SerializedName("hashtags")
public java.util.List<Tag> hashtags;
public Pagination pagination;
}

View File

@ -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;

View File

@ -56,6 +56,7 @@ public class FragmentMastodonAccount extends Fragment {
private boolean flagLoading;
private List<Account> 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;

View File

@ -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<Tag> 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<Tag> 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);
}
}
}
});
}
}

View File

@ -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<Status> 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())

View File

@ -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<Results> 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<Results> 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) {

View File

@ -198,11 +198,11 @@ public class TimelinesVM extends AndroidViewModel {
return statusesMutableLiveData;
}
public LiveData<List<Tag>> getTagsTrends(String token, @NonNull String instance) {
public LiveData<List<Tag>> getTagsTrends(String token, @NonNull String instance, Integer offset, Integer limit) {
MastodonTimelinesService mastodonTimelinesService = init(instance);
tagListMutableLiveData = new MutableLiveData<>();
new Thread(() -> {
Call<List<Tag>> publicTlCall = mastodonTimelinesService.getTagTrends(token);
Call<List<Tag>> publicTlCall = mastodonTimelinesService.getTagTrends(token, offset, limit);
List<Tag> tagList = null;
if (publicTlCall != null) {
try {

View File

@ -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