Explorar o código

Add support for endless loading

Signed-off-by: Mario Danic <mario@lovelyhq.com>
Mario Danic %!s(int64=7) %!d(string=hai) anos
pai
achega
aa8d058ef8

+ 144 - 0
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 <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
+    }
+
+}

+ 2 - 2
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<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);
 
 
     /*

+ 168 - 84
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<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());
+                            }
+
+                            if (shareesOverall.getOcs().getData().getExactUsers() != null &&
+                                    shareesOverall.getOcs().getData().getExactUsers().getExactSharees() != null) {
+                                shareeHashSet.addAll(shareesOverall.getOcs().getData().
+                                        getExactUsers().getExactSharees());
+                            }
 
-                                        headerTitle = sharee.getLabel().substring(0, 1).toUpperCase();
+                            Participant participant;
+                            for (Sharee sharee : shareeHashSet) {
+                                if (!sharee.getValue().getShareWith().equals(currentUser.getUsername())) {
+                                    participant = new Participant();
+                                    participant.setName(sharee.getLabel());
+                                    String headerTitle;
 
-                                        UserHeaderItem userHeaderItem;
-                                        if (!userHeaderItems.containsKey(headerTitle)) {
-                                            userHeaderItem = new UserHeaderItem(headerTitle);
-                                            userHeaderItems.put(headerTitle, userHeaderItem);
-                                        }
+                                    headerTitle = sharee.getLabel().substring(0, 1).toUpperCase();
 
-                                        participant.setUserId(sharee.getValue().getShareWith());
-                                        contactItems.add(new UserItem(participant, currentUser,
-                                                userHeaderItems.get(headerTitle)));
+                                    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));
 
-                                userHeaderItems = new HashMap<>();
+                                    if (!contactItems.contains(newContactItem)) {
+                                        contactItems.add(newContactItem);
+                                    }
 
-                                Collections.sort(contactItems, (o1, o2) -> {
-                                    String firstName;
-                                    String secondName;
+                                }
 
-                                    if (o1 instanceof UserItem) {
-                                        firstName = ((UserItem) o1).getModel().getName();
-                                    } else {
-                                        firstName = ((UserHeaderItem) o1).getModel();
-                                    }
+                            }
 
-                                    if (o2 instanceof UserItem) {
-                                        secondName = ((UserItem) o2).getModel().getName();
-                                    } else {
-                                        secondName = ((UserHeaderItem) o2).getModel();
-                                    }
 
-                                    return firstName.compareToIgnoreCase(secondName);
-                                });
+                            userHeaderItems = new HashMap<>();
 
-                                adapter.updateDataSet(contactItems, true);
-                                searchItem.setVisible(contactItems.size() > 0);
-                                swipeRefreshLayout.setRefreshing(false);
+                            Collections.sort(contactItems, (o1, o2) -> {
+                                String firstName;
+                                String secondName;
 
+                                if (o1 instanceof UserItem) {
+                                    firstName = ((UserItem) o1).getModel().getName();
+                                } else {
+                                    firstName = ((UserHeaderItem) o1).getModel();
+                                }
 
-                                if (isNewConversationView) {
-                                    checkAndHandleBottomButtons();
+                                if (o2 instanceof UserItem) {
+                                    secondName = ((UserItem) o2).getModel().getName();
+                                } else {
+                                    secondName = ((UserHeaderItem) o2).getModel();
                                 }
-                            }
 
-                        }, throwable -> {
-                            if (searchItem != null) {
-                                searchItem.setVisible(false);
+                                return firstName.compareToIgnoreCase(secondName);
+                            });
+
+                            if (startFromScratch) {
+                                adapter.updateDataSet(contactItems, true);
+                            } else {
+                                adapter.onLoadMoreComplete(null);
+                                adapter = new FlexibleAdapter<>(contactItems, getActivity(), false);
+                                recyclerView.setAdapter(adapter);
+                                setupAdapter();
+                                adapter.notifyDataSetChanged();
                             }
+                            searchItem.setVisible(contactItems.size() > 0);
+                            swipeRefreshLayout.setRefreshing(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;
-                                }
+
+                            if (isNewConversationView) {
+                                checkAndHandleBottomButtons();
                             }
+                        }
+                    }
 
-                            swipeRefreshLayout.setRefreshing(false);
-                            dispose(contactsQueryDisposable);
+                    @Override
+                    public void onError(Throwable e) {
+                        if (searchItem != null) {
+                            searchItem.setVisible(false);
                         }
-                        , () -> {
-                            swipeRefreshLayout.setRefreshing(false);
-                            dispose(contactsQueryDisposable);
-                        });
+
+                        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;
+        }
+    }
 }

+ 0 - 1
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);

+ 45 - 0
app/src/main/res/layout/rv_item_progress.xml

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

+ 6 - 0
app/src/main/res/values/strings.xml

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