Commit f6b9138a authored by Yusuke Iwaki's avatar Yusuke Iwaki

FIX #56 implement "load more"

parent 5c6e524c
......@@ -238,7 +238,7 @@ public class MethodCallHelper {
/**
* Load messages for room.
*/
public Task<Void> loadHistory(final String roomId, final long timestamp,
public Task<JSONArray> loadHistory(final String roomId, final long timestamp,
final int count, final long lastSeen) {
return call("loadHistory", TIMEOUT_MS, () -> new JSONArray()
.put(roomId)
......@@ -261,7 +261,7 @@ public class MethodCallHelper {
realm.createOrUpdateAllFromJson(Message.class, messages);
}
return null;
});
}).onSuccessTask(_task -> Task.forResult(messages));
});
}
......
......@@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import chat.rocket.android.R;
import chat.rocket.android.helper.LoadMoreScrollListener;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.layouthelper.chatroom.MessageListAdapter;
import chat.rocket.android.model.ServerConfig;
......@@ -15,10 +16,10 @@ import chat.rocket.android.model.internal.LoadMessageProcedure;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.realm_helper.RealmObjectObserver;
import chat.rocket.android.realm_helper.RealmStore;
import io.realm.Realm;
import io.realm.RealmResults;
import chat.rocket.android.service.RocketChatService;
import io.realm.Sort;
import org.json.JSONObject;
import timber.log.Timber;
/**
* Chat room screen.
......@@ -29,6 +30,8 @@ public class RoomFragment extends AbstractChatRoomFragment {
private String roomId;
private RealmObjectObserver<RoomSubscription> roomObserver;
private String hostname;
private LoadMoreScrollListener scrollListener;
private RealmObjectObserver<LoadMessageProcedure> procedureObserver;
/**
* create fragment with roomId.
......@@ -61,7 +64,13 @@ public class RoomFragment extends AbstractChatRoomFragment {
.createObjectObserver(realm -> realm.where(RoomSubscription.class).equalTo("rid", roomId))
.setOnUpdateListener(this::onRenderRoom);
initialRequest();
procedureObserver = realmHelper
.createObjectObserver(realm ->
realm.where(LoadMessageProcedure.class).equalTo("roomId", roomId))
.setOnUpdateListener(this::onUpdateLoadMessageProcedure);
if (savedInstanceState == null) {
initialRequest();
}
}
@Override protected int getLayout() {
......@@ -77,36 +86,84 @@ public class RoomFragment extends AbstractChatRoomFragment {
context -> new MessageListAdapter(context, hostname)
));
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(),
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(),
LinearLayoutManager.VERTICAL, true);
listView.setLayoutManager(layoutManager);
scrollListener = new LoadMoreScrollListener(layoutManager, 40) {
@Override public void requestMoreItem() {
loadMoreRequest();
}
};
listView.addOnScrollListener(scrollListener);
}
private void onRenderRoom(RoomSubscription roomSubscription) {
activityToolbar.setTitle(roomSubscription.getName());
}
private void onUpdateLoadMessageProcedure(LoadMessageProcedure procedure) {
if (procedure == null) {
return;
}
RecyclerView listView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
if (listView != null && listView.getAdapter() instanceof MessageListAdapter) {
MessageListAdapter adapter = (MessageListAdapter) listView.getAdapter();
final int syncstate = procedure.getSyncstate();
final boolean hasNext = procedure.isHasNext();
Timber.d("hasNext: %s syncstate: %d", hasNext, syncstate);
if (syncstate == SyncState.SYNCED || syncstate == SyncState.FAILED) {
scrollListener.setLoadingDone();
adapter.updateFooter(hasNext, true);
} else {
adapter.updateFooter(hasNext, false);
}
}
}
private void initialRequest() {
realmHelper.executeTransaction(realm -> {
realm.createOrUpdateObjectFromJson(LoadMessageProcedure.class, new JSONObject()
.put("roomId", roomId)
.put("syncstate", SyncState.NOT_SYNCED)
.put("count", 50)
.put("count", 100)
.put("reset", true));
return null;
}).onSuccessTask(task -> {
RocketChatService.keepalive(getContext());
return task;
}).continueWith(new LogcatIfError());
}
private RealmResults<Message> queryItems(Realm realm) {
return realm.where(Message.class).equalTo("rid", roomId).findAllSorted("ts");
private void loadMoreRequest() {
realmHelper.executeTransaction(realm -> {
LoadMessageProcedure procedure = realm.where(LoadMessageProcedure.class)
.equalTo("roomId", roomId)
.beginGroup()
.equalTo("syncstate", SyncState.SYNCED)
.or()
.equalTo("syncstate", SyncState.FAILED)
.endGroup()
.equalTo("hasNext", true)
.findFirst();
if (procedure != null) {
procedure.setSyncstate(SyncState.NOT_SYNCED);
}
return null;
}).onSuccessTask(task -> {
RocketChatService.keepalive(getContext());
return task;
}).continueWith(new LogcatIfError());
}
@Override public void onResume() {
super.onResume();
roomObserver.sub();
procedureObserver.sub();
}
@Override public void onPause() {
procedureObserver.unsub();
roomObserver.unsub();
super.onPause();
}
......
package chat.rocket.android.helper;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
public abstract class LoadMoreScrollListener extends RecyclerView.OnScrollListener {
private final LinearLayoutManager layoutManager;
private final int loadThreshold;
private boolean isLoading;
public LoadMoreScrollListener(LinearLayoutManager layoutManager, int loadThreshold) {
this.layoutManager = layoutManager;
this.loadThreshold = loadThreshold;
setLoadingDone();
}
@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
final int visibleItemCount = recyclerView.getChildCount();
final int totalItemCount = layoutManager.getItemCount();
final int firstVisibleItem = layoutManager.findFirstVisibleItemPosition();
if (!isLoading
&& firstVisibleItem + visibleItemCount >= totalItemCount - loadThreshold
&& visibleItemCount < totalItemCount
&& dy < 0) {
isLoading = true;
requestMoreItem();
}
}
public void setLoadingDone() {
isLoading = false;
}
public abstract void requestMoreItem();
}
\ No newline at end of file
package chat.rocket.android.layouthelper;
import android.content.Context;
import android.support.annotation.LayoutRes;
import chat.rocket.android.realm_helper.RealmModelListAdapter;
import chat.rocket.android.realm_helper.RealmModelViewHolder;
import io.realm.RealmObject;
/**
* RealmModelListAdapter with header and footer.
*/
public abstract class ExtRealmModelListAdapter<T extends RealmObject, VM,
VH extends RealmModelViewHolder<VM>> extends RealmModelListAdapter<T, VM, VH> {
protected static final int VIEW_TYPE_HEADER = -1;
protected static final int VIEW_TYPE_FOOTER = -2;
protected ExtRealmModelListAdapter(Context context) {
super(context);
}
@Override public int getItemCount() {
return super.getItemCount() + 2;
}
protected void notifyHeaderChanged() {
notifyItemChanged(0);
}
protected void notifyFooterChanged() {
notifyItemChanged(super.getItemCount() + 1);
}
protected void notifyRealmModelItemChanged(int position) {
notifyItemChanged(position + 1);
}
@Override public int getItemViewType(int position) {
if (position == 0) {
return VIEW_TYPE_HEADER;
}
if (position == super.getItemCount() + 1) {
return VIEW_TYPE_FOOTER;
}
// rely on getRealmModelViewType(VM model).
return super.getItemViewType(position - 1);
}
protected abstract @LayoutRes int getHeaderLayout();
protected abstract @LayoutRes int getFooterLayout();
protected abstract @LayoutRes int getRealmModelLayout(int viewType);
@Override protected final int getLayout(int viewType) {
if (viewType == VIEW_TYPE_HEADER) {
return getHeaderLayout();
}
if (viewType == VIEW_TYPE_FOOTER) {
return getFooterLayout();
}
return getRealmModelLayout(viewType);
}
@Override public final void onBindViewHolder(VH holder, int position) {
if (position == 0 || position == super.getItemCount() + 1) {
return;
}
// rely on VH.bind().
super.onBindViewHolder(holder, position - 1);
}
}
......@@ -3,8 +3,8 @@ package chat.rocket.android.layouthelper.chatroom;
import android.content.Context;
import android.view.View;
import chat.rocket.android.R;
import chat.rocket.android.layouthelper.ExtRealmModelListAdapter;
import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.realm_helper.RealmModelListAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
......@@ -13,20 +13,42 @@ import java.util.List;
* target list adapter for chat room.
*/
public class MessageListAdapter
extends RealmModelListAdapter<Message, PairedMessage, MessageViewHolder> {
extends ExtRealmModelListAdapter<Message, PairedMessage, MessageViewHolder> {
private final String hostname;
private boolean hasNext;
private boolean isLoaded;
public MessageListAdapter(Context context, String hostname) {
super(context);
this.hostname = hostname;
}
/**
* update Footer state considering hasNext and isLoaded.
*/
public void updateFooter(boolean hasNext, boolean isLoaded) {
this.hasNext = hasNext;
this.isLoaded = isLoaded;
notifyFooterChanged();
}
@Override protected int getHeaderLayout() {
return R.layout.list_item_message_header;
}
@Override protected int getFooterLayout() {
if (!hasNext || isLoaded) {
return R.layout.list_item_message_start_of_conversation;
} else {
return R.layout.list_item_message_loading;
}
}
@Override protected int getRealmModelViewType(PairedMessage model) {
return 0;
}
@Override protected int getLayout(int viewType) {
@Override protected int getRealmModelLayout(int viewType) {
return R.layout.list_item_message;
}
......
......@@ -57,14 +57,16 @@ public class LoadMessageProcedureObserver extends AbstractModelObserver<LoadMess
realm.where(Message.class)
.equalTo("rid", roomId)
.equalTo("syncstate", SyncState.SYNCED)
.findAllSorted("ts", Sort.ASCENDING).last(null));
.findAllSorted("ts", Sort.ASCENDING).first(null));
long lastTs = lastMessage != null ? lastMessage.getTs() : 0;
int messageCount = _task.getResult().length();
return realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(LoadMessageProcedure.class, new JSONObject()
.put("roomId", roomId)
.put("syncstate", SyncState.SYNCED)
.put("timestamp", lastTs)
.put("hasNext", lastTs > 0)));
.put("reset", false)
.put("hasNext", messageCount == count)));
})
).continueWithTask(task -> {
if (task.isFaulted()) {
......
......@@ -10,7 +10,7 @@
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_compose"
android:id="@+id/fab_compose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
......
<?xml version="1.0" encoding="utf-8"?>
<Space
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/space"
android:layout_width="match_parent"
android:layout_height="88dp"
/>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme"
>
<chat.rocket.android.widget.WaitingView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/margin_8"
/>
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_16"
android:text="@string/start_of_conversation"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:layout_gravity="center"
/>
</FrameLayout>
\ No newline at end of file
......@@ -7,4 +7,6 @@
<string name="user_status_busy">Busy</string>
<string name="user_status_invisible">Invisible</string>
<string name="fragment_sidebar_main_logout_title">Logout</string>
<string name="start_of_conversation">Start of conversation</string>
</resources>
......@@ -16,7 +16,7 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM,
RealmModelListAdapter<T, VM, VH> getNewInstance(Context context);
}
private final LayoutInflater inflater;
protected final LayoutInflater inflater;
private RealmListObserver<T> realmListObserver;
private List<VM> adapterData;
......@@ -54,11 +54,11 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM,
protected abstract List<VM> mapResultsToViewModel(List<T> results);
@Override public final int getItemViewType(int position) {
@Override public int getItemViewType(int position) {
return getRealmModelViewType(getItem(position));
}
@Override public VH onCreateViewHolder(ViewGroup parent, int viewType) {
@Override public final VH onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = inflater.inflate(getLayout(viewType), parent, false);
return onCreateRealmModelViewHolder(viewType, itemView);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment