Add support for endless loading

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2018-05-16 17:36:02 +02:00
parent b984eea07a
commit aa8d058ef8
6 changed files with 369 additions and 91 deletions

View file

@ -0,0 +1,144 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Heavily copied from and influenced by
* https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-On-Load-More#automatic-load-more.
* Author: David Steduto under Apache2 licence
*/
package com.nextcloud.talk.adapters.items;
import android.animation.Animator;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.nextcloud.talk.R;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.Payload;
import eu.davidea.flexibleadapter.helpers.AnimatorHelper;
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
import eu.davidea.flexibleadapter.items.IFlexible;
import eu.davidea.viewholders.FlexibleViewHolder;
/**
* @author Davide Steduto
* @since 22/04/2016
*/
public class ProgressItem extends AbstractFlexibleItem<ProgressItem.ProgressViewHolder> {
private StatusEnum status = StatusEnum.MORE_TO_LOAD;
@Override
public boolean equals(Object o) {
return this == o;//The default implementation
}
public StatusEnum getStatus() {
return status;
}
public void setStatus(StatusEnum status) {
this.status = status;
}
@Override
public int getLayoutRes() {
return R.layout.rv_item_progress;
}
@Override
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ProgressViewHolder holder, int position, List<Object> payloads) {
Context context = holder.itemView.getContext();
holder.progressBar.setVisibility(View.GONE);
holder.progressMessage.setVisibility(View.VISIBLE);
if (!adapter.isEndlessScrollEnabled()) {
setStatus(StatusEnum.DISABLE_ENDLESS);
} else if (payloads.contains(Payload.NO_MORE_LOAD)) {
setStatus(StatusEnum.NO_MORE_LOAD);
}
switch (this.status) {
case NO_MORE_LOAD:
holder.progressMessage.setText(
context.getString(R.string.nc_no_more_load_retry));
// Reset to default status for next binding
setStatus(StatusEnum.MORE_TO_LOAD);
break;
case DISABLE_ENDLESS:
holder.progressMessage.setText(context.getString(R.string.nc_endless_disabled));
break;
case ON_CANCEL:
holder.progressMessage.setText(context.getString(R.string.nc_endless_cancel));
// Reset to default status for next binding
setStatus(StatusEnum.MORE_TO_LOAD);
break;
case ON_ERROR:
holder.progressMessage.setText(context.getString(R.string.nc_endless_error));
// Reset to default status for next binding
setStatus(StatusEnum.MORE_TO_LOAD);
break;
default:
holder.progressBar.setVisibility(View.VISIBLE);
holder.progressMessage.setVisibility(View.GONE);
break;
}
}
@Override
public ProgressViewHolder createViewHolder(View view, FlexibleAdapter adapter) {
return new ProgressViewHolder(view, adapter);
}
static class ProgressViewHolder extends FlexibleViewHolder {
@BindView(R.id.progress_bar)
ProgressBar progressBar;
@BindView(R.id.progress_message)
TextView progressMessage;
ProgressViewHolder(View view, FlexibleAdapter adapter) {
super(view, adapter);
ButterKnife.bind(this, view);
}
@Override
public void scrollAnimators(@NonNull List<Animator> animators, int position, boolean isForward) {
AnimatorHelper.scaleAnimator(animators, itemView, 0f);
}
}
public enum StatusEnum {
MORE_TO_LOAD, //Default = should have an empty Payload
DISABLE_ENDLESS, //Endless is disabled because user has set limits
NO_MORE_LOAD, //Non-empty Payload = Payload.NO_MORE_LOAD
ON_CANCEL,
ON_ERROR
}
}

View file

@ -66,8 +66,8 @@ public interface NcApi {
Server URL is: baseUrl + ocsApiVersion + /apps/files_sharing/api/v1/sharees
*/
@GET
Observable<ShareesOverall> getContactsWithSearchParam(@Header("Authorization") String authorization, @Url String url,
@QueryMap Map<String, String> options);
Observable<Response<ShareesOverall>> getContactsWithSearchParam(@Header("Authorization") String authorization, @Url String url,
@QueryMap Map<String, Object> options);
/*

View file

@ -52,6 +52,7 @@ import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
import com.kennyc.bottomsheet.BottomSheet;
import com.nextcloud.talk.R;
import com.nextcloud.talk.activities.CallActivity;
import com.nextcloud.talk.adapters.items.ProgressItem;
import com.nextcloud.talk.adapters.items.UserHeaderItem;
import com.nextcloud.talk.adapters.items.UserItem;
import com.nextcloud.talk.api.NcApi;
@ -81,6 +82,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
@ -101,10 +103,11 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import retrofit2.HttpException;
import retrofit2.Response;
@AutoInjector(NextcloudTalkApplication.class)
public class ContactsController extends BaseController implements SearchView.OnQueryTextListener,
FlexibleAdapter.OnItemClickListener, FastScroller.OnScrollStateChangeListener {
FlexibleAdapter.OnItemClickListener, FastScroller.OnScrollStateChangeListener, FlexibleAdapter.EndlessScrollListener {
public static final String TAG = "ContactsController";
@ -144,6 +147,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ
private List<AbstractFlexibleItem> contactItems = new ArrayList<>();
private BottomSheet bottomSheet;
private View view;
private int currentPage;
private SmoothScrollLinearLayoutManager layoutManager;
@ -156,6 +160,9 @@ public class ContactsController extends BaseController implements SearchView.OnQ
private HashMap<String, UserHeaderItem> userHeaderItems = new HashMap<>();
private boolean alreadyFetching = false;
private boolean canFetchFurther = true;
public ContactsController() {
super();
setHasOptionsMenu(true);
@ -213,21 +220,29 @@ public class ContactsController extends BaseController implements SearchView.OnQ
if (adapter == null) {
adapter = new FlexibleAdapter<>(contactItems, getActivity(), false);
adapter.setNotifyChangeOfUnfilteredItems(true)
.setMode(SelectableAdapter.Mode.MULTI);
if (currentUser != null) {
fetchData();
fetchData(true);
}
}
setupAdapter();
prepareViews();
}
private void setupAdapter() {
adapter.setNotifyChangeOfUnfilteredItems(true)
.setMode(SelectableAdapter.Mode.MULTI);
adapter.setEndlessScrollListener(this, new ProgressItem());
adapter.setStickyHeaderElevation(5)
.setUnlinkAllItemsOnRemoveHeaders(true)
.setDisplayHeadersAtStartUp(true)
.setStickyHeaders(true);
adapter.addListener(this);
prepareViews();
}
@Optional
@ -408,119 +423,168 @@ public class ContactsController extends BaseController implements SearchView.OnQ
}
private void fetchData() {
private void fetchData(boolean startFromScratch) {
dispose(null);
alreadyFetching = true;
Set<Sharee> shareeHashSet = new HashSet<>();
contactItems = new ArrayList<>();
if (startFromScratch) {
contactItems = new ArrayList<>();
}
userHeaderItems = new HashMap<>();
RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForContactsSearch(currentUser.getBaseUrl(),
"");
contactsQueryDisposable = ncApi.getContactsWithSearchParam(
int page = 1;
if (!startFromScratch) {
page = currentPage + 1;
}
Map<String, Object> modifiedQueryMap = new HashMap<>(retrofitBucket.getQueryMap());
modifiedQueryMap.put("page", page);
modifiedQueryMap.put("perPage", 100);
ncApi.getContactsWithSearchParam(
ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
retrofitBucket.getUrl(), modifiedQueryMap)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.retry(3)
.subscribe((ShareesOverall shareesOverall) -> {
if (shareesOverall != null) {
.subscribe(new Observer<Response>() {
@Override
public void onSubscribe(Disposable d) {
contactsQueryDisposable = d;
}
if (shareesOverall.getOcs().getData().getUsers() != null) {
shareeHashSet.addAll(shareesOverall.getOcs().getData().getUsers());
}
@Override
public void onNext(Response response) {
canFetchFurther = response.headers().size() > 0 &&
!TextUtils.isEmpty((response.headers().get("Link")));
if (response.body() != null) {
ShareesOverall shareesOverall = (ShareesOverall) response.body();
if (shareesOverall.getOcs().getData().getExactUsers() != null &&
shareesOverall.getOcs().getData().getExactUsers().getExactSharees() != null) {
shareeHashSet.addAll(shareesOverall.getOcs().getData().
getExactUsers().getExactSharees());
}
currentPage = (int) modifiedQueryMap.get("page");
Participant participant;
for (Sharee sharee : shareeHashSet) {
if (!sharee.getValue().getShareWith().equals(currentUser.getUsername())) {
participant = new Participant();
participant.setName(sharee.getLabel());
String headerTitle;
if (shareesOverall.getOcs().getData().getUsers() != null) {
shareeHashSet.addAll(shareesOverall.getOcs().getData().getUsers());
}
headerTitle = sharee.getLabel().substring(0, 1).toUpperCase();
if (shareesOverall.getOcs().getData().getExactUsers() != null &&
shareesOverall.getOcs().getData().getExactUsers().getExactSharees() != null) {
shareeHashSet.addAll(shareesOverall.getOcs().getData().
getExactUsers().getExactSharees());
}
UserHeaderItem userHeaderItem;
if (!userHeaderItems.containsKey(headerTitle)) {
userHeaderItem = new UserHeaderItem(headerTitle);
userHeaderItems.put(headerTitle, userHeaderItem);
}
Participant participant;
for (Sharee sharee : shareeHashSet) {
if (!sharee.getValue().getShareWith().equals(currentUser.getUsername())) {
participant = new Participant();
participant.setName(sharee.getLabel());
String headerTitle;
participant.setUserId(sharee.getValue().getShareWith());
contactItems.add(new UserItem(participant, currentUser,
userHeaderItems.get(headerTitle)));
headerTitle = sharee.getLabel().substring(0, 1).toUpperCase();
UserHeaderItem userHeaderItem;
if (!userHeaderItems.containsKey(headerTitle)) {
userHeaderItem = new UserHeaderItem(headerTitle);
userHeaderItems.put(headerTitle, userHeaderItem);
}
participant.setUserId(sharee.getValue().getShareWith());
UserItem newContactItem = new UserItem(participant, currentUser,
userHeaderItems.get(headerTitle));
if (!contactItems.contains(newContactItem)) {
contactItems.add(newContactItem);
}
}
}
userHeaderItems = new HashMap<>();
Collections.sort(contactItems, (o1, o2) -> {
String firstName;
String secondName;
userHeaderItems = new HashMap<>();
if (o1 instanceof UserItem) {
firstName = ((UserItem) o1).getModel().getName();
} else {
firstName = ((UserHeaderItem) o1).getModel();
}
Collections.sort(contactItems, (o1, o2) -> {
String firstName;
String secondName;
if (o2 instanceof UserItem) {
secondName = ((UserItem) o2).getModel().getName();
} else {
secondName = ((UserHeaderItem) o2).getModel();
}
if (o1 instanceof UserItem) {
firstName = ((UserItem) o1).getModel().getName();
} else {
firstName = ((UserHeaderItem) o1).getModel();
}
return firstName.compareToIgnoreCase(secondName);
});
if (o2 instanceof UserItem) {
secondName = ((UserItem) o2).getModel().getName();
} else {
secondName = ((UserHeaderItem) o2).getModel();
}
return firstName.compareToIgnoreCase(secondName);
});
if (startFromScratch) {
adapter.updateDataSet(contactItems, true);
searchItem.setVisible(contactItems.size() > 0);
swipeRefreshLayout.setRefreshing(false);
if (isNewConversationView) {
checkAndHandleBottomButtons();
}
} else {
adapter.onLoadMoreComplete(null);
adapter = new FlexibleAdapter<>(contactItems, getActivity(), false);
recyclerView.setAdapter(adapter);
setupAdapter();
adapter.notifyDataSetChanged();
}
}, throwable -> {
if (searchItem != null) {
searchItem.setVisible(false);
}
if (throwable instanceof HttpException) {
HttpException exception = (HttpException) throwable;
switch (exception.code()) {
case 401:
if (getParentController() != null &&
getParentController().getRouter() != null) {
getParentController().getRouter().pushController((RouterTransaction.with
(new WebViewLoginController(currentUser.getBaseUrl(),
true))
.pushChangeHandler(new VerticalChangeHandler())
.popChangeHandler(new VerticalChangeHandler())));
}
break;
default:
break;
}
}
searchItem.setVisible(contactItems.size() > 0);
swipeRefreshLayout.setRefreshing(false);
dispose(contactsQueryDisposable);
if (isNewConversationView) {
checkAndHandleBottomButtons();
}
}
, () -> {
swipeRefreshLayout.setRefreshing(false);
dispose(contactsQueryDisposable);
});
}
@Override
public void onError(Throwable e) {
if (searchItem != null) {
searchItem.setVisible(false);
}
if (e instanceof HttpException) {
HttpException exception = (HttpException) e;
switch (exception.code()) {
case 401:
if (getParentController() != null &&
getParentController().getRouter() != null) {
getParentController().getRouter().pushController((RouterTransaction.with
(new WebViewLoginController(currentUser.getBaseUrl(),
true))
.pushChangeHandler(new VerticalChangeHandler())
.popChangeHandler(new VerticalChangeHandler())));
}
break;
default:
break;
}
}
swipeRefreshLayout.setRefreshing(false);
dispose(contactsQueryDisposable);
}
@Override
public void onComplete() {
swipeRefreshLayout.setRefreshing(false);
dispose(contactsQueryDisposable);
alreadyFetching = false;
}
});
}
private void prepareViews() {
@ -529,7 +593,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ
recyclerView.setHasFixedSize(true);
recyclerView.setAdapter(adapter);
swipeRefreshLayout.setOnRefreshListener(this::fetchData);
swipeRefreshLayout.setOnRefreshListener(() -> fetchData(true));
swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary);
fastScroller.addOnScrollStateChangeListener(this);
@ -538,8 +602,10 @@ public class ContactsController extends BaseController implements SearchView.OnQ
IFlexible abstractFlexibleItem = adapter.getItem(position);
if (abstractFlexibleItem instanceof UserItem) {
return ((UserItem) adapter.getItem(position)).getHeader().getModel();
} else {
} else if (abstractFlexibleItem instanceof UserHeaderItem) {
return ((UserHeaderItem) adapter.getItem(position)).getModel();
} else {
return "";
}
});
@ -779,4 +845,22 @@ public class ContactsController extends BaseController implements SearchView.OnQ
secondaryRelativeLayout.setVisibility(View.VISIBLE);
}
}
@Override
public void noMoreLoad(int newItemsSize) {
}
@Override
public void onLoadMore(int lastPosition, int currentPage) {
if (adapter.hasFilter()) {
adapter.onLoadMoreComplete(null);
return;
}
if (!alreadyFetching && canFetchFurther) {
fetchData(false);
} else {
return;
}
}
}

View file

@ -50,7 +50,6 @@ public class ApiUtils {
queryMap.put("format", "json");
queryMap.put("search", searchQuery);
queryMap.put("perPage", "200");
queryMap.put("itemType", "call");
retrofitBucket.setQueryMap(queryMap);

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:padding="8dp">
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"/>
<TextView
android:id="@+id/progress_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_no_more_load_retry"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible"/>
</FrameLayout>

View file

@ -175,4 +175,10 @@ Find Nextcloud on https://nextcloud.com</string>
<string name="nc_conversation_menu_video_call">Video call</string>
<string name="nc_new_messages">New messages</string>
<!-- Contacts endless loading -->
<string name="nc_no_more_load_retry">No more items to load. Refresh to retry.</string>
<string name="nc_endless_disabled">No more items to load (max reached).</string>
<string name="nc_endless_cancel">Cancelled by the user.</string>
<string name="nc_endless_error">An error occurred while loading more items.</string>
</resources>