Commit f6b9138a authored by Yusuke Iwaki's avatar Yusuke Iwaki

FIX #56 implement "load more"

parent 5c6e524c
...@@ -238,7 +238,7 @@ public class MethodCallHelper { ...@@ -238,7 +238,7 @@ public class MethodCallHelper {
/** /**
* Load messages for room. * 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) { final int count, final long lastSeen) {
return call("loadHistory", TIMEOUT_MS, () -> new JSONArray() return call("loadHistory", TIMEOUT_MS, () -> new JSONArray()
.put(roomId) .put(roomId)
...@@ -261,7 +261,7 @@ public class MethodCallHelper { ...@@ -261,7 +261,7 @@ public class MethodCallHelper {
realm.createOrUpdateAllFromJson(Message.class, messages); realm.createOrUpdateAllFromJson(Message.class, messages);
} }
return null; return null;
}); }).onSuccessTask(_task -> Task.forResult(messages));
}); });
} }
......
...@@ -5,6 +5,7 @@ import android.support.annotation.Nullable; ...@@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.helper.LoadMoreScrollListener;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.layouthelper.chatroom.MessageListAdapter; import chat.rocket.android.layouthelper.chatroom.MessageListAdapter;
import chat.rocket.android.model.ServerConfig; import chat.rocket.android.model.ServerConfig;
...@@ -15,10 +16,10 @@ import chat.rocket.android.model.internal.LoadMessageProcedure; ...@@ -15,10 +16,10 @@ import chat.rocket.android.model.internal.LoadMessageProcedure;
import chat.rocket.android.realm_helper.RealmHelper; import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.realm_helper.RealmObjectObserver; import chat.rocket.android.realm_helper.RealmObjectObserver;
import chat.rocket.android.realm_helper.RealmStore; import chat.rocket.android.realm_helper.RealmStore;
import io.realm.Realm; import chat.rocket.android.service.RocketChatService;
import io.realm.RealmResults;
import io.realm.Sort; import io.realm.Sort;
import org.json.JSONObject; import org.json.JSONObject;
import timber.log.Timber;
/** /**
* Chat room screen. * Chat room screen.
...@@ -29,6 +30,8 @@ public class RoomFragment extends AbstractChatRoomFragment { ...@@ -29,6 +30,8 @@ public class RoomFragment extends AbstractChatRoomFragment {
private String roomId; private String roomId;
private RealmObjectObserver<RoomSubscription> roomObserver; private RealmObjectObserver<RoomSubscription> roomObserver;
private String hostname; private String hostname;
private LoadMoreScrollListener scrollListener;
private RealmObjectObserver<LoadMessageProcedure> procedureObserver;
/** /**
* create fragment with roomId. * create fragment with roomId.
...@@ -61,7 +64,13 @@ public class RoomFragment extends AbstractChatRoomFragment { ...@@ -61,7 +64,13 @@ public class RoomFragment extends AbstractChatRoomFragment {
.createObjectObserver(realm -> realm.where(RoomSubscription.class).equalTo("rid", roomId)) .createObjectObserver(realm -> realm.where(RoomSubscription.class).equalTo("rid", roomId))
.setOnUpdateListener(this::onRenderRoom); .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() { @Override protected int getLayout() {
...@@ -77,36 +86,84 @@ public class RoomFragment extends AbstractChatRoomFragment { ...@@ -77,36 +86,84 @@ public class RoomFragment extends AbstractChatRoomFragment {
context -> new MessageListAdapter(context, hostname) context -> new MessageListAdapter(context, hostname)
)); ));
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(),
LinearLayoutManager.VERTICAL, true); LinearLayoutManager.VERTICAL, true);
listView.setLayoutManager(layoutManager); listView.setLayoutManager(layoutManager);
scrollListener = new LoadMoreScrollListener(layoutManager, 40) {
@Override public void requestMoreItem() {
loadMoreRequest();
}
};
listView.addOnScrollListener(scrollListener);
} }
private void onRenderRoom(RoomSubscription roomSubscription) { private void onRenderRoom(RoomSubscription roomSubscription) {
activityToolbar.setTitle(roomSubscription.getName()); 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() { private void initialRequest() {
realmHelper.executeTransaction(realm -> { realmHelper.executeTransaction(realm -> {
realm.createOrUpdateObjectFromJson(LoadMessageProcedure.class, new JSONObject() realm.createOrUpdateObjectFromJson(LoadMessageProcedure.class, new JSONObject()
.put("roomId", roomId) .put("roomId", roomId)
.put("syncstate", SyncState.NOT_SYNCED) .put("syncstate", SyncState.NOT_SYNCED)
.put("count", 50) .put("count", 100)
.put("reset", true)); .put("reset", true));
return null; return null;
}).onSuccessTask(task -> {
RocketChatService.keepalive(getContext());
return task;
}).continueWith(new LogcatIfError()); }).continueWith(new LogcatIfError());
} }
private RealmResults<Message> queryItems(Realm realm) { private void loadMoreRequest() {
return realm.where(Message.class).equalTo("rid", roomId).findAllSorted("ts"); 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() { @Override public void onResume() {
super.onResume(); super.onResume();
roomObserver.sub(); roomObserver.sub();
procedureObserver.sub();
} }
@Override public void onPause() { @Override public void onPause() {
procedureObserver.unsub();
roomObserver.unsub(); roomObserver.unsub();
super.onPause(); 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; ...@@ -3,8 +3,8 @@ package chat.rocket.android.layouthelper.chatroom;
import android.content.Context; import android.content.Context;
import android.view.View; import android.view.View;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.layouthelper.ExtRealmModelListAdapter;
import chat.rocket.android.model.ddp.Message; import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.realm_helper.RealmModelListAdapter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
...@@ -13,20 +13,42 @@ import java.util.List; ...@@ -13,20 +13,42 @@ import java.util.List;
* target list adapter for chat room. * target list adapter for chat room.
*/ */
public class MessageListAdapter public class MessageListAdapter
extends RealmModelListAdapter<Message, PairedMessage, MessageViewHolder> { extends ExtRealmModelListAdapter<Message, PairedMessage, MessageViewHolder> {
private final String hostname; private final String hostname;
private boolean hasNext;
private boolean isLoaded;
public MessageListAdapter(Context context, String hostname) { public MessageListAdapter(Context context, String hostname) {
super(context); super(context);
this.hostname = hostname; 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) { @Override protected int getRealmModelViewType(PairedMessage model) {
return 0; return 0;
} }
@Override protected int getLayout(int viewType) { @Override protected int getRealmModelLayout(int viewType) {
return R.layout.list_item_message; return R.layout.list_item_message;
} }
......
...@@ -57,14 +57,16 @@ public class LoadMessageProcedureObserver extends AbstractModelObserver<LoadMess ...@@ -57,14 +57,16 @@ public class LoadMessageProcedureObserver extends AbstractModelObserver<LoadMess
realm.where(Message.class) realm.where(Message.class)
.equalTo("rid", roomId) .equalTo("rid", roomId)
.equalTo("syncstate", SyncState.SYNCED) .equalTo("syncstate", SyncState.SYNCED)
.findAllSorted("ts", Sort.ASCENDING).last(null)); .findAllSorted("ts", Sort.ASCENDING).first(null));
long lastTs = lastMessage != null ? lastMessage.getTs() : 0; long lastTs = lastMessage != null ? lastMessage.getTs() : 0;
int messageCount = _task.getResult().length();
return realmHelper.executeTransaction(realm -> return realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(LoadMessageProcedure.class, new JSONObject() realm.createOrUpdateObjectFromJson(LoadMessageProcedure.class, new JSONObject()
.put("roomId", roomId) .put("roomId", roomId)
.put("syncstate", SyncState.SYNCED) .put("syncstate", SyncState.SYNCED)
.put("timestamp", lastTs) .put("timestamp", lastTs)
.put("hasNext", lastTs > 0))); .put("reset", false)
.put("hasNext", messageCount == count)));
}) })
).continueWithTask(task -> { ).continueWithTask(task -> {
if (task.isFaulted()) { if (task.isFaulted()) {
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
/> />
<android.support.design.widget.FloatingActionButton <android.support.design.widget.FloatingActionButton
android:id="@+id/btn_compose" android:id="@+id/fab_compose"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|bottom" 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 @@ ...@@ -7,4 +7,6 @@
<string name="user_status_busy">Busy</string> <string name="user_status_busy">Busy</string>
<string name="user_status_invisible">Invisible</string> <string name="user_status_invisible">Invisible</string>
<string name="fragment_sidebar_main_logout_title">Logout</string> <string name="fragment_sidebar_main_logout_title">Logout</string>
<string name="start_of_conversation">Start of conversation</string>
</resources> </resources>
...@@ -16,7 +16,7 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM, ...@@ -16,7 +16,7 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM,
RealmModelListAdapter<T, VM, VH> getNewInstance(Context context); RealmModelListAdapter<T, VM, VH> getNewInstance(Context context);
} }
private final LayoutInflater inflater; protected final LayoutInflater inflater;
private RealmListObserver<T> realmListObserver; private RealmListObserver<T> realmListObserver;
private List<VM> adapterData; private List<VM> adapterData;
...@@ -54,11 +54,11 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM, ...@@ -54,11 +54,11 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM,
protected abstract List<VM> mapResultsToViewModel(List<T> results); 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)); 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); View itemView = inflater.inflate(getLayout(viewType), parent, false);
return onCreateRealmModelViewHolder(viewType, itemView); 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