diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ProgressItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ProgressItem.java new file mode 100644 index 000000000..143a6e51d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ProgressItem.java @@ -0,0 +1,144 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2018 Mario Danic + * + * 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 . + * + * 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 { + + 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 adapter, ProgressViewHolder holder, int position, List 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 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 53b6b00c2..5eee57506 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -66,8 +66,8 @@ public interface NcApi { Server URL is: baseUrl + ocsApiVersion + /apps/files_sharing/api/v1/sharees */ @GET - Observable getContactsWithSearchParam(@Header("Authorization") String authorization, @Url String url, - @QueryMap Map options); + Observable> getContactsWithSearchParam(@Header("Authorization") String authorization, @Url String url, + @QueryMap Map options); /* diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java index 8266b45a5..1c4c915d9 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java @@ -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 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 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 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 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() { + @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; + } + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 7783a80af..befe90154 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -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); diff --git a/app/src/main/res/layout/rv_item_progress.xml b/app/src/main/res/layout/rv_item_progress.xml new file mode 100644 index 000000000..b47b7d298 --- /dev/null +++ b/app/src/main/res/layout/rv_item_progress.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38a5a6c07..0354c2a48 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -175,4 +175,10 @@ Find Nextcloud on https://nextcloud.com Video call New messages + + No more items to load. Refresh to retry. + No more items to load (max reached). + Cancelled by the user. + An error occurred while loading more items. +