mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 22:38:37 +00:00
Fix conversation list bug with pinned chats.
Co-authored-by: Alex Hart <alex@signal.org>
This commit is contained in:
parent
f84c8229de
commit
e428453835
@ -1,148 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversationlist;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.paging.PagedList;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapter;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
class CompositeConversationListAdapter extends RecyclerViewConcatenateAdapter {
|
|
||||||
|
|
||||||
private final FixedViewsAdapter pinnedHeaderAdapter;
|
|
||||||
private final ConversationListAdapter pinnedAdapter;
|
|
||||||
private final FixedViewsAdapter unpinnedHeaderAdapter;
|
|
||||||
private final ConversationListAdapter unpinnedAdapter;
|
|
||||||
|
|
||||||
CompositeConversationListAdapter(@NonNull RecyclerView rv,
|
|
||||||
@NonNull GlideRequests glideRequests,
|
|
||||||
@NonNull ConversationListAdapter.OnConversationClickListener onConversationClickListener)
|
|
||||||
{
|
|
||||||
|
|
||||||
TextView pinned = (TextView) LayoutInflater.from(rv.getContext()).inflate(R.layout.conversation_list_item_header, rv, false);
|
|
||||||
TextView unpinned = (TextView) LayoutInflater.from(rv.getContext()).inflate(R.layout.conversation_list_item_header, rv, false);
|
|
||||||
|
|
||||||
pinned.setText(rv.getContext().getString(R.string.conversation_list__pinned));
|
|
||||||
unpinned.setText(rv.getContext().getString(R.string.conversation_list__chats));
|
|
||||||
|
|
||||||
this.pinnedHeaderAdapter = new FixedViewsAdapter(pinned);
|
|
||||||
this.pinnedAdapter = new ConversationListAdapter(this, glideRequests, onConversationClickListener);
|
|
||||||
this.unpinnedHeaderAdapter = new FixedViewsAdapter(unpinned);
|
|
||||||
this.unpinnedAdapter = new ConversationListAdapter(this, glideRequests, onConversationClickListener);
|
|
||||||
|
|
||||||
pinnedHeaderAdapter.hide();
|
|
||||||
unpinnedHeaderAdapter.hide();
|
|
||||||
|
|
||||||
unpinnedAdapter.registerAdapterDataObserver(new UnpinnedAdapterDataObserver());
|
|
||||||
pinnedAdapter.registerAdapterDataObserver(new PinnedAdapterDataObserver());
|
|
||||||
|
|
||||||
addAdapter(pinnedHeaderAdapter);
|
|
||||||
addAdapter(pinnedAdapter);
|
|
||||||
addAdapter(unpinnedHeaderAdapter);
|
|
||||||
addAdapter(unpinnedAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void submitPinnedList(@NonNull PagedList<Conversation> pinnedConversations) {
|
|
||||||
pinnedAdapter.submitList(pinnedConversations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void submitUnpinnedList(@NonNull PagedList<Conversation> unpinnedConversations) {
|
|
||||||
unpinnedAdapter.submitList(unpinnedConversations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTypingThreads(@NonNull Set<Long> threads) {
|
|
||||||
pinnedAdapter.setTypingThreads(threads);
|
|
||||||
unpinnedAdapter.setTypingThreads(threads);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NonNull Set<Long> getBatchSelectionIds() {
|
|
||||||
HashSet<Long> hashSet = new HashSet();
|
|
||||||
|
|
||||||
hashSet.addAll(pinnedAdapter.getBatchSelectionIds());
|
|
||||||
hashSet.addAll(unpinnedAdapter.getBatchSelectionIds());
|
|
||||||
|
|
||||||
return hashSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void selectAllThreads() {
|
|
||||||
pinnedAdapter.selectAllThreads();
|
|
||||||
unpinnedAdapter.selectAllThreads();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateArchived(int archivedCount) {
|
|
||||||
unpinnedAdapter.updateArchived(archivedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void toggleConversationInBatchSet(@NonNull Conversation conversation) {
|
|
||||||
if (conversation.getThreadRecord().isPinned()) {
|
|
||||||
pinnedAdapter.toggleConversationInBatchSet(conversation);
|
|
||||||
} else {
|
|
||||||
unpinnedAdapter.toggleConversationInBatchSet(conversation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initializeBatchMode(boolean toggle) {
|
|
||||||
pinnedAdapter.initializeBatchMode(toggle);
|
|
||||||
unpinnedAdapter.initializeBatchMode(toggle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getPinnedItemCount() {
|
|
||||||
return pinnedAdapter.getItemCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NonNull Collection<Conversation> getBatchSelection() {
|
|
||||||
Set<Conversation> conversations = new HashSet<>();
|
|
||||||
|
|
||||||
conversations.addAll(pinnedAdapter.getBatchSelection());
|
|
||||||
conversations.addAll(unpinnedAdapter.getBatchSelection());
|
|
||||||
|
|
||||||
return conversations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UnpinnedAdapterDataObserver extends RecyclerView.AdapterDataObserver {
|
|
||||||
@Override
|
|
||||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
|
||||||
if (unpinnedAdapter.getItemCount() == 0) {
|
|
||||||
unpinnedHeaderAdapter.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
|
||||||
if (itemCount > 0 && pinnedAdapter.getItemCount() > 0) {
|
|
||||||
unpinnedHeaderAdapter.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PinnedAdapterDataObserver extends RecyclerView.AdapterDataObserver {
|
|
||||||
@Override
|
|
||||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
|
||||||
if (pinnedAdapter.getItemCount() == 0) {
|
|
||||||
pinnedHeaderAdapter.hide();
|
|
||||||
unpinnedHeaderAdapter.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
|
||||||
if (itemCount > 0) {
|
|
||||||
pinnedHeaderAdapter.show();
|
|
||||||
|
|
||||||
if (unpinnedAdapter.getItemCount() > 0) {
|
|
||||||
unpinnedHeaderAdapter.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.paging.PagedListAdapter;
|
import androidx.paging.PagedListAdapter;
|
||||||
@ -13,12 +14,9 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapter;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -30,11 +28,12 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
class ConversationListAdapter extends PagedListAdapter<Conversation, ConversationListAdapter.BaseViewHolder> {
|
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
private static final int TYPE_THREAD = 1;
|
private static final int TYPE_THREAD = 1;
|
||||||
private static final int TYPE_ACTION = 2;
|
private static final int TYPE_ACTION = 2;
|
||||||
private static final int TYPE_PLACEHOLDER = 3;
|
private static final int TYPE_PLACEHOLDER = 3;
|
||||||
|
private static final int TYPE_HEADER = 4;
|
||||||
|
|
||||||
private enum Payload {
|
private enum Payload {
|
||||||
TYPING_INDICATOR,
|
TYPING_INDICATOR,
|
||||||
@ -46,26 +45,21 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
private final Map<Long, Conversation> batchSet = Collections.synchronizedMap(new HashMap<>());
|
private final Map<Long, Conversation> batchSet = Collections.synchronizedMap(new HashMap<>());
|
||||||
private boolean batchMode = false;
|
private boolean batchMode = false;
|
||||||
private final Set<Long> typingSet = new HashSet<>();
|
private final Set<Long> typingSet = new HashSet<>();
|
||||||
private int archived;
|
|
||||||
|
|
||||||
private final RecyclerViewConcatenateAdapter parent;
|
protected ConversationListAdapter(@NonNull GlideRequests glideRequests,
|
||||||
|
|
||||||
protected ConversationListAdapter(@NonNull RecyclerViewConcatenateAdapter parent,
|
|
||||||
@NonNull GlideRequests glideRequests,
|
|
||||||
@NonNull OnConversationClickListener onConversationClickListener)
|
@NonNull OnConversationClickListener onConversationClickListener)
|
||||||
{
|
{
|
||||||
super(new ConversationDiffCallback());
|
super(new ConversationDiffCallback());
|
||||||
|
|
||||||
this.parent = parent;
|
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.onConversationClickListener = onConversationClickListener;
|
this.onConversationClickListener = onConversationClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
if (viewType == TYPE_ACTION) {
|
if (viewType == TYPE_ACTION) {
|
||||||
ConversationViewHolder holder = new ConversationViewHolder(LayoutInflater.from(parent.getContext())
|
ConversationViewHolder holder = new ConversationViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.conversation_list_item_action, parent, false), viewType);
|
.inflate(R.layout.conversation_list_item_action, parent, false));
|
||||||
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
holder.itemView.setOnClickListener(v -> {
|
||||||
if (holder.getAdapterPosition() != RecyclerView.NO_POSITION) {
|
if (holder.getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||||
@ -76,10 +70,10 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
return holder;
|
return holder;
|
||||||
} else if (viewType == TYPE_THREAD) {
|
} else if (viewType == TYPE_THREAD) {
|
||||||
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
|
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.conversation_list_item_view, parent, false), viewType);
|
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||||
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
holder.itemView.setOnClickListener(v -> {
|
||||||
int position = this.parent.getLocalPosition(holder.getAdapterPosition()).getLocalPosition();
|
int position = holder.getAdapterPosition();
|
||||||
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
onConversationClickListener.onConversationClick(getItem(position));
|
onConversationClickListener.onConversationClick(getItem(position));
|
||||||
@ -87,7 +81,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
});
|
});
|
||||||
|
|
||||||
holder.itemView.setOnLongClickListener(v -> {
|
holder.itemView.setOnLongClickListener(v -> {
|
||||||
int position = this.parent.getLocalPosition(holder.getAdapterPosition()).getLocalPosition();
|
int position = holder.getAdapterPosition();
|
||||||
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
return onConversationClickListener.onConversationLongClick(getItem(position));
|
return onConversationClickListener.onConversationLongClick(getItem(position));
|
||||||
@ -100,16 +94,19 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
View v = new FrameLayout(parent.getContext());
|
View v = new FrameLayout(parent.getContext());
|
||||||
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
|
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
|
||||||
return new PlaceholderViewHolder(v);
|
return new PlaceholderViewHolder(v);
|
||||||
|
} else if (viewType == TYPE_HEADER) {
|
||||||
|
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_list_item_header, parent, false);
|
||||||
|
return new HeaderViewHolder(v);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Unknown type! " + viewType);
|
throw new IllegalStateException("Unknown type! " + viewType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position, @NonNull List<Object> payloads) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||||
if (payloads.isEmpty()) {
|
if (payloads.isEmpty()) {
|
||||||
onBindViewHolder(holder, position);
|
onBindViewHolder(holder, position);
|
||||||
} else {
|
} else if (holder instanceof ConversationViewHolder) {
|
||||||
for (Object payloadObject : payloads) {
|
for (Object payloadObject : payloads) {
|
||||||
if (payloadObject instanceof Payload) {
|
if (payloadObject instanceof Payload) {
|
||||||
Payload payload = (Payload) payloadObject;
|
Payload payload = (Payload) payloadObject;
|
||||||
@ -125,22 +122,8 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||||
if (holder.getLocalViewType() == TYPE_ACTION) {
|
if (holder.getItemViewType() == TYPE_ACTION || holder.getItemViewType() == TYPE_THREAD) {
|
||||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
|
||||||
|
|
||||||
casted.getConversationListItem().bind(new ThreadRecord.Builder(100)
|
|
||||||
.setBody("")
|
|
||||||
.setDate(100)
|
|
||||||
.setRecipient(Recipient.UNKNOWN)
|
|
||||||
.setCount(archived)
|
|
||||||
.build(),
|
|
||||||
glideRequests,
|
|
||||||
Locale.getDefault(),
|
|
||||||
typingSet,
|
|
||||||
getBatchSelectionIds(),
|
|
||||||
batchMode);
|
|
||||||
} else if (holder.getLocalViewType() == TYPE_THREAD) {
|
|
||||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
||||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||||
|
|
||||||
@ -150,11 +133,24 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
typingSet,
|
typingSet,
|
||||||
getBatchSelectionIds(),
|
getBatchSelectionIds(),
|
||||||
batchMode);
|
batchMode);
|
||||||
|
} else if (holder.getItemViewType() == TYPE_HEADER) {
|
||||||
|
HeaderViewHolder casted = (HeaderViewHolder) holder;
|
||||||
|
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||||
|
switch (conversation.getType()) {
|
||||||
|
case PINNED_HEADER:
|
||||||
|
casted.headerText.setText(R.string.conversation_list__pinned);
|
||||||
|
break;
|
||||||
|
case UNPINNED_HEADER:
|
||||||
|
casted.headerText.setText(R.string.conversation_list__chats);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewRecycled(@NonNull BaseViewHolder holder) {
|
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||||
if (holder instanceof ConversationViewHolder) {
|
if (holder instanceof ConversationViewHolder) {
|
||||||
((ConversationViewHolder) holder).getConversationListItem().unbind();
|
((ConversationViewHolder) holder).getConversationListItem().unbind();
|
||||||
}
|
}
|
||||||
@ -181,35 +177,22 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
return batchSet.values();
|
return batchSet.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateArchived(int archived) {
|
|
||||||
int oldArchived = this.archived;
|
|
||||||
|
|
||||||
this.archived = archived;
|
|
||||||
|
|
||||||
if (oldArchived != archived) {
|
|
||||||
if (archived == 0) {
|
|
||||||
notifyItemRemoved(getItemCount());
|
|
||||||
} else if (oldArchived == 0) {
|
|
||||||
notifyItemInserted(getItemCount() - 1);
|
|
||||||
} else {
|
|
||||||
notifyItemChanged(getItemCount() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return (archived > 0 ? 1 : 0) + super.getItemCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (archived > 0 && position == getItemCount() - 1) {
|
Conversation conversation = getItem(position);
|
||||||
return TYPE_ACTION;
|
if (conversation == null) {
|
||||||
} else if (getItem(position) == null) {
|
|
||||||
return TYPE_PLACEHOLDER;
|
return TYPE_PLACEHOLDER;
|
||||||
} else {
|
}
|
||||||
return TYPE_THREAD;
|
switch (conversation.getType()) {
|
||||||
|
case PINNED_HEADER:
|
||||||
|
case UNPINNED_HEADER:
|
||||||
|
return TYPE_HEADER;
|
||||||
|
case ARCHIVED_FOOTER:
|
||||||
|
return TYPE_ACTION;
|
||||||
|
case THREAD:
|
||||||
|
return TYPE_THREAD;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +203,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
void selectAllThreads() {
|
void selectAllThreads() {
|
||||||
for (int i = 0; i < super.getItemCount(); i++) {
|
for (int i = 0; i < super.getItemCount(); i++) {
|
||||||
Conversation conversation = getItem(i);
|
Conversation conversation = getItem(i);
|
||||||
if (conversation != null && conversation.getThreadRecord().getThreadId() != -1) {
|
if (conversation != null && conversation.getThreadRecord().getThreadId() >= 0) {
|
||||||
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,26 +222,12 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class BaseViewHolder extends RecyclerView.ViewHolder {
|
static final class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private final int viewType;
|
|
||||||
|
|
||||||
public BaseViewHolder(@NonNull View itemView, int viewType) {
|
|
||||||
super(itemView);
|
|
||||||
this.viewType = viewType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLocalViewType() {
|
|
||||||
return viewType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class ConversationViewHolder extends BaseViewHolder {
|
|
||||||
|
|
||||||
private final BindableConversationListItem conversationListItem;
|
private final BindableConversationListItem conversationListItem;
|
||||||
|
|
||||||
ConversationViewHolder(@NonNull View itemView, int viewType) {
|
ConversationViewHolder(@NonNull View itemView) {
|
||||||
super(itemView, viewType);
|
super(itemView);
|
||||||
|
|
||||||
conversationListItem = (BindableConversationListItem) itemView;
|
conversationListItem = (BindableConversationListItem) itemView;
|
||||||
}
|
}
|
||||||
@ -281,9 +250,18 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class PlaceholderViewHolder extends BaseViewHolder {
|
private static class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
||||||
PlaceholderViewHolder(@NonNull View itemView) {
|
PlaceholderViewHolder(@NonNull View itemView) {
|
||||||
super(itemView, TYPE_PLACEHOLDER);
|
super(itemView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
private TextView headerText;
|
||||||
|
|
||||||
|
public HeaderViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
headerText = (TextView) itemView;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,16 @@ package org.thoughtcrime.securesms.conversationlist;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.database.MergeCursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.paging.DataSource;
|
import androidx.paging.DataSource;
|
||||||
import androidx.paging.PositionalDataSource;
|
import androidx.paging.PositionalDataSource;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
@ -57,10 +61,9 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||||||
context.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, contentObserver);
|
context.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, contentObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isPinned, boolean isArchived) {
|
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||||
if (isPinned) return new PinnedConversationListDataSource(context, invalidator);
|
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
|
||||||
else if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
|
else return new ArchivedConversationListDataSource(context, invalidator);
|
||||||
else return new ArchivedConversationListDataSource(context, invalidator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -72,7 +75,7 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||||||
int effectiveCount = params.requestedStartPosition;
|
int effectiveCount = params.requestedStartPosition;
|
||||||
List<Recipient> recipients = new LinkedList<>();
|
List<Recipient> recipients = new LinkedList<>();
|
||||||
|
|
||||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
|
try (ConversationReader reader = new ConversationReader(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
|
||||||
ThreadRecord record;
|
ThreadRecord record;
|
||||||
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
|
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
|
||||||
conversations.add(new Conversation(record));
|
conversations.add(new Conversation(record));
|
||||||
@ -99,7 +102,7 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||||||
List<Conversation> conversations = new ArrayList<>(params.loadSize);
|
List<Conversation> conversations = new ArrayList<>(params.loadSize);
|
||||||
List<Recipient> recipients = new LinkedList<>();
|
List<Recipient> recipients = new LinkedList<>();
|
||||||
|
|
||||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.startPosition, params.loadSize))) {
|
try (ConversationReader reader = new ConversationReader(getCursor(params.startPosition, params.loadSize))) {
|
||||||
ThreadRecord record;
|
ThreadRecord record;
|
||||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||||
conversations.add(new Conversation(record));
|
conversations.add(new Conversation(record));
|
||||||
@ -134,7 +137,13 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class UnarchivedConversationListDataSource extends ConversationListDataSource {
|
@VisibleForTesting
|
||||||
|
static class UnarchivedConversationListDataSource extends ConversationListDataSource {
|
||||||
|
|
||||||
|
private int totalCount;
|
||||||
|
private int pinnedCount;
|
||||||
|
private int archivedCount;
|
||||||
|
private int unpinnedCount;
|
||||||
|
|
||||||
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
||||||
super(context, invalidator);
|
super(context, invalidator);
|
||||||
@ -142,29 +151,69 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getTotalCount() {
|
protected int getTotalCount() {
|
||||||
return threadDatabase.getUnpinnedConversationListCount();
|
int unarchivedCount = threadDatabase.getUnarchivedConversationListCount();
|
||||||
|
|
||||||
|
pinnedCount = threadDatabase.getPinnedConversationListCount();
|
||||||
|
archivedCount = threadDatabase.getArchivedConversationListCount();
|
||||||
|
unpinnedCount = unarchivedCount - pinnedCount;
|
||||||
|
totalCount = unarchivedCount + (archivedCount != 0 ? 1 : 0) + (pinnedCount != 0 ? (unpinnedCount != 0 ? 2 : 1) : 0);
|
||||||
|
|
||||||
|
return totalCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Cursor getCursor(long offset, long limit) {
|
protected Cursor getCursor(long offset, long limit) {
|
||||||
return threadDatabase.getUnpinnedConversationList(offset, limit);
|
List<Cursor> cursors = new ArrayList<>(5);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class PinnedConversationListDataSource extends ConversationListDataSource {
|
if (offset == 0 && hasPinnedHeader()) {
|
||||||
|
MatrixCursor pinnedHeaderCursor = new MatrixCursor(ConversationReader.HEADER_COLUMN);
|
||||||
|
pinnedHeaderCursor.addRow(ConversationReader.PINNED_HEADER);
|
||||||
|
cursors.add(pinnedHeaderCursor);
|
||||||
|
limit--;
|
||||||
|
}
|
||||||
|
|
||||||
protected PinnedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
Cursor pinnedCursor = threadDatabase.getUnarchivedConversationList(true, offset, limit);
|
||||||
super(context, invalidator);
|
cursors.add(pinnedCursor);
|
||||||
|
limit -= pinnedCursor.getCount();
|
||||||
|
|
||||||
|
if (offset == 0 && hasUnpinnedHeader()) {
|
||||||
|
MatrixCursor unpinnedHeaderCursor = new MatrixCursor(ConversationReader.HEADER_COLUMN);
|
||||||
|
unpinnedHeaderCursor.addRow(ConversationReader.UNPINNED_HEADER);
|
||||||
|
cursors.add(unpinnedHeaderCursor);
|
||||||
|
limit--;
|
||||||
|
}
|
||||||
|
|
||||||
|
long unpinnedOffset = Math.max(0, offset - pinnedCount - getHeaderOffset());
|
||||||
|
Cursor unpinnedCursor = threadDatabase.getUnarchivedConversationList(false, unpinnedOffset, limit);
|
||||||
|
cursors.add(unpinnedCursor);
|
||||||
|
|
||||||
|
if (offset + limit >= totalCount && hasArchivedFooter()) {
|
||||||
|
MatrixCursor archivedFooterCursor = new MatrixCursor(ConversationReader.ARCHIVED_COLUMNS);
|
||||||
|
archivedFooterCursor.addRow(ConversationReader.createArchivedFooterRow(archivedCount));
|
||||||
|
cursors.add(archivedFooterCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MergeCursor(cursors.toArray(new Cursor[]{}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@VisibleForTesting
|
||||||
protected int getTotalCount() {
|
int getHeaderOffset() {
|
||||||
return threadDatabase.getPinnedConversationListCount();
|
return (hasPinnedHeader() ? 1 : 0) + (hasUnpinnedHeader() ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@VisibleForTesting
|
||||||
protected Cursor getCursor(long offset, long limit) {
|
boolean hasPinnedHeader() {
|
||||||
return threadDatabase.getPinnedConversationList(offset, limit);
|
return pinnedCount != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
boolean hasUnpinnedHeader() {
|
||||||
|
return hasPinnedHeader() && unpinnedCount != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
boolean hasArchivedFooter() {
|
||||||
|
return archivedCount != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,19 +221,17 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Invalidator invalidator;
|
private final Invalidator invalidator;
|
||||||
private final boolean isPinned;
|
|
||||||
private final boolean isArchived;
|
private final boolean isArchived;
|
||||||
|
|
||||||
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isPinned, boolean isArchived) {
|
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.invalidator = invalidator;
|
this.invalidator = invalidator;
|
||||||
this.isPinned = isPinned;
|
|
||||||
this.isArchived = isArchived;
|
this.isArchived = isArchived;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull DataSource<Integer, Conversation> create() {
|
public @NonNull DataSource<Integer, Conversation> create() {
|
||||||
return ConversationListDataSource.create(context, invalidator, isPinned, isArchived);
|
return ConversationListDataSource.create(context, invalidator, isArchived);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
private View toolbarShadow;
|
private View toolbarShadow;
|
||||||
private ConversationListViewModel viewModel;
|
private ConversationListViewModel viewModel;
|
||||||
private RecyclerView.Adapter activeAdapter;
|
private RecyclerView.Adapter activeAdapter;
|
||||||
private CompositeConversationListAdapter defaultAdapter;
|
private ConversationListAdapter defaultAdapter;
|
||||||
private ConversationListSearchAdapter searchAdapter;
|
private ConversationListSearchAdapter searchAdapter;
|
||||||
private StickyHeaderDecoration searchAdapterDecoration;
|
private StickyHeaderDecoration searchAdapterDecoration;
|
||||||
private ViewGroup megaphoneContainer;
|
private ViewGroup megaphoneContainer;
|
||||||
@ -213,7 +213,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
|
|
||||||
reminderView.setOnDismissListener(this::updateReminders);
|
reminderView.setOnDismissListener(this::updateReminders);
|
||||||
|
|
||||||
list.setHasFixedSize(true);
|
|
||||||
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
|
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
|
||||||
list.setItemAnimator(new DeleteItemAnimator());
|
list.setItemAnimator(new DeleteItemAnimator());
|
||||||
list.addOnScrollListener(new ScrollListener());
|
list.addOnScrollListener(new ScrollListener());
|
||||||
@ -458,7 +457,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeListAdapters() {
|
private void initializeListAdapters() {
|
||||||
defaultAdapter = new CompositeConversationListAdapter(list, GlideApp.with(this), this);
|
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
|
||||||
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
|
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
|
||||||
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
|
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
|
||||||
|
|
||||||
@ -503,8 +502,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
|
|
||||||
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
|
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
|
||||||
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
||||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitUnpinnedList);
|
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList);
|
||||||
viewModel.getPinnedConversations().observe(getViewLifecycleOwner(), this::onSubmitPinnedList);
|
|
||||||
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||||
@ -749,7 +747,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
.map(conversation -> conversation.getThreadRecord().getThreadId())
|
.map(conversation -> conversation.getThreadRecord().getThreadId())
|
||||||
.toList());
|
.toList());
|
||||||
|
|
||||||
if (toPin.size() + defaultAdapter.getPinnedItemCount() > MAXIMUM_PINNED_CONVERSATIONS) {
|
if (toPin.size() + viewModel.getPinnedCount() > MAXIMUM_PINNED_CONVERSATIONS) {
|
||||||
Snackbar.make(fab,
|
Snackbar.make(fab,
|
||||||
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
|
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
|
||||||
Snackbar.LENGTH_LONG)
|
Snackbar.LENGTH_LONG)
|
||||||
@ -789,13 +787,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSubmitPinnedList(@NonNull PagedList<Conversation> pinnedConversations) {
|
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
||||||
defaultAdapter.submitPinnedList(pinnedConversations);
|
defaultAdapter.submitList(conversationList.getConversations());
|
||||||
}
|
|
||||||
|
|
||||||
private void onSubmitUnpinnedList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
|
||||||
defaultAdapter.submitUnpinnedList(conversationList.getConversations());
|
|
||||||
defaultAdapter.updateArchived(conversationList.getArchivedCount());
|
|
||||||
|
|
||||||
onPostSubmitList();
|
onPostSubmitList();
|
||||||
}
|
}
|
||||||
@ -926,7 +919,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||||||
private void setCorrectMenuVisibility(@NonNull Menu menu) {
|
private void setCorrectMenuVisibility(@NonNull Menu menu) {
|
||||||
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
||||||
boolean hasUnpinned = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned());
|
boolean hasUnpinned = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned());
|
||||||
boolean canPin = defaultAdapter.getPinnedItemCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
||||||
|
|
||||||
if (hasUnread) {
|
if (hasUnread) {
|
||||||
menu.findItem(R.id.menu_mark_as_unread).setVisible(false);
|
menu.findItem(R.id.menu_mark_as_unread).setVisible(false);
|
||||||
|
@ -31,6 +31,8 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
|||||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
class ConversationListViewModel extends ViewModel {
|
class ConversationListViewModel extends ViewModel {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(ConversationListViewModel.class);
|
private static final String TAG = Log.tag(ConversationListViewModel.class);
|
||||||
@ -38,7 +40,6 @@ class ConversationListViewModel extends ViewModel {
|
|||||||
private final Application application;
|
private final Application application;
|
||||||
private final MutableLiveData<Megaphone> megaphone;
|
private final MutableLiveData<Megaphone> megaphone;
|
||||||
private final MutableLiveData<SearchResult> searchResult;
|
private final MutableLiveData<SearchResult> searchResult;
|
||||||
private final LiveData<PagedList<Conversation>> pinnedList;
|
|
||||||
private final LiveData<ConversationList> conversationList;
|
private final LiveData<ConversationList> conversationList;
|
||||||
private final SearchRepository searchRepository;
|
private final SearchRepository searchRepository;
|
||||||
private final MegaphoneRepository megaphoneRepository;
|
private final MegaphoneRepository megaphoneRepository;
|
||||||
@ -65,7 +66,7 @@ class ConversationListViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, false, isArchived);
|
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
|
||||||
PagedList.Config config = new PagedList.Config.Builder()
|
PagedList.Config config = new PagedList.Config.Builder()
|
||||||
.setPageSize(15)
|
.setPageSize(15)
|
||||||
.setInitialLoadSizeHint(30)
|
.setInitialLoadSizeHint(30)
|
||||||
@ -87,36 +88,21 @@ class ConversationListViewModel extends ViewModel {
|
|||||||
MutableLiveData<ConversationList> updated = new MutableLiveData<>();
|
MutableLiveData<ConversationList> updated = new MutableLiveData<>();
|
||||||
|
|
||||||
if (isArchived) {
|
if (isArchived) {
|
||||||
updated.postValue(new ConversationList(conversation, 0));
|
updated.postValue(new ConversationList(conversation, 0, 0));
|
||||||
} else {
|
} else {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
int archiveCount = DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount();
|
int archiveCount = DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount();
|
||||||
updated.postValue(new ConversationList(conversation, archiveCount));
|
int pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
|
||||||
|
updated.postValue(new ConversationList(conversation, archiveCount, pinnedCount));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isArchived) {
|
|
||||||
DataSource.Factory<Integer, Conversation> pinnedFactory = new ConversationListDataSource.Factory(application, invalidator, true, false);
|
|
||||||
|
|
||||||
this.pinnedList = new LivePagedListBuilder<>(pinnedFactory, config).setFetchExecutor(ConversationListDataSource.EXECUTOR)
|
|
||||||
.setInitialLoadKey(0)
|
|
||||||
.build();
|
|
||||||
} else {
|
|
||||||
this.pinnedList = new MutableLiveData<>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Boolean> hasNoConversations() {
|
public LiveData<Boolean> hasNoConversations() {
|
||||||
return LiveDataUtil.combineLatest(getPinnedConversations(),
|
return Transformations.map(getConversationList(), ConversationList::isEmpty);
|
||||||
getConversationList(),
|
|
||||||
(pinned, unpinned) -> pinned.isEmpty() && unpinned.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull LiveData<PagedList<Conversation>> getPinnedConversations() {
|
|
||||||
return pinnedList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull LiveData<SearchResult> getSearchResult() {
|
@NonNull LiveData<SearchResult> getSearchResult() {
|
||||||
@ -131,6 +117,10 @@ class ConversationListViewModel extends ViewModel {
|
|||||||
return conversationList;
|
return conversationList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getPinnedCount() {
|
||||||
|
return Objects.requireNonNull(getConversationList().getValue()).pinnedCount;
|
||||||
|
}
|
||||||
|
|
||||||
void onVisible() {
|
void onVisible() {
|
||||||
megaphoneRepository.getNextMegaphone(megaphone::postValue);
|
megaphoneRepository.getNextMegaphone(megaphone::postValue);
|
||||||
}
|
}
|
||||||
@ -189,10 +179,12 @@ class ConversationListViewModel extends ViewModel {
|
|||||||
final static class ConversationList {
|
final static class ConversationList {
|
||||||
private final PagedList<Conversation> conversations;
|
private final PagedList<Conversation> conversations;
|
||||||
private final int archivedCount;
|
private final int archivedCount;
|
||||||
|
private final int pinnedCount;
|
||||||
|
|
||||||
ConversationList(PagedList<Conversation> conversations, int archivedCount) {
|
ConversationList(PagedList<Conversation> conversations, int archivedCount, int pinnedCount) {
|
||||||
this.conversations = conversations;
|
this.conversations = conversations;
|
||||||
this.archivedCount = archivedCount;
|
this.archivedCount = archivedCount;
|
||||||
|
this.pinnedCount = pinnedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
PagedList<Conversation> getConversations() {
|
PagedList<Conversation> getConversations() {
|
||||||
@ -203,6 +195,10 @@ class ConversationListViewModel extends ViewModel {
|
|||||||
return archivedCount;
|
return archivedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getPinnedCount() {
|
||||||
|
return pinnedCount;
|
||||||
|
}
|
||||||
|
|
||||||
boolean isEmpty() {
|
boolean isEmpty() {
|
||||||
return conversations.isEmpty() && archivedCount == 0;
|
return conversations.isEmpty() && archivedCount == 0;
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,25 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
|||||||
|
|
||||||
public class Conversation {
|
public class Conversation {
|
||||||
private final ThreadRecord threadRecord;
|
private final ThreadRecord threadRecord;
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
public Conversation(@NonNull ThreadRecord threadRecord) {
|
public Conversation(@NonNull ThreadRecord threadRecord) {
|
||||||
this.threadRecord = threadRecord;
|
this.threadRecord = threadRecord;
|
||||||
|
if (this.threadRecord.getThreadId() < 0) {
|
||||||
|
type = Type.valueOf(this.threadRecord.getBody());
|
||||||
|
} else {
|
||||||
|
type = Type.THREAD;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull ThreadRecord getThreadRecord() {
|
public @NonNull ThreadRecord getThreadRecord() {
|
||||||
return threadRecord;
|
return threadRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @NonNull Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
@ -27,4 +37,11 @@ public class Conversation {
|
|||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return threadRecord.hashCode();
|
return threadRecord.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
THREAD,
|
||||||
|
PINNED_HEADER,
|
||||||
|
UNPINNED_HEADER,
|
||||||
|
ARCHIVED_FOOTER
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversationlist.model;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||||
|
|
||||||
|
public class ConversationReader extends ThreadDatabase.StaticReader {
|
||||||
|
|
||||||
|
public static final String[] HEADER_COLUMN = {"header"};
|
||||||
|
public static final String[] ARCHIVED_COLUMNS = {"header", "count"};
|
||||||
|
public static final String[] PINNED_HEADER = {Conversation.Type.PINNED_HEADER.toString()};
|
||||||
|
public static final String[] UNPINNED_HEADER = {Conversation.Type.UNPINNED_HEADER.toString()};
|
||||||
|
|
||||||
|
private final Cursor cursor;
|
||||||
|
|
||||||
|
public ConversationReader(@NonNull Cursor cursor) {
|
||||||
|
super(cursor, ApplicationDependencies.getApplication());
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] createArchivedFooterRow(int archivedCount) {
|
||||||
|
return new String[]{Conversation.Type.ARCHIVED_FOOTER.toString(), String.valueOf(archivedCount)};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ThreadRecord getCurrent() {
|
||||||
|
if (cursor.getColumnIndex(HEADER_COLUMN[0]) == -1) {
|
||||||
|
return super.getCurrent();
|
||||||
|
} else {
|
||||||
|
return buildThreadRecordForHeader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ThreadRecord buildThreadRecordForHeader() {
|
||||||
|
Conversation.Type type = Conversation.Type.valueOf(CursorUtil.requireString(cursor, HEADER_COLUMN[0]));
|
||||||
|
int count = 0;
|
||||||
|
if (type == Conversation.Type.ARCHIVED_FOOTER) {
|
||||||
|
count = CursorUtil.requireInt(cursor, ARCHIVED_COLUMNS[1]);
|
||||||
|
}
|
||||||
|
return new ThreadRecord.Builder(-(100 + type.ordinal()))
|
||||||
|
.setBody(type.toString())
|
||||||
|
.setDate(100)
|
||||||
|
.setRecipient(Recipient.UNKNOWN)
|
||||||
|
.setCount(count)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@ -606,7 +606,7 @@ public final class GroupDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable GroupRecord getCurrent() {
|
public @Nullable GroupRecord getCurrent() {
|
||||||
if (cursor == null || cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)) == null) {
|
if (cursor == null || cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)) == null || cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)) == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +143,8 @@ public class ThreadDatabase extends Database {
|
|||||||
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
|
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
private static final String ORDER_BY_DEFAULT = TABLE_NAME + "." + DATE + " DESC";
|
||||||
|
|
||||||
public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
super(context, databaseHelper);
|
super(context, databaseHelper);
|
||||||
}
|
}
|
||||||
@ -542,7 +544,12 @@ public class ThreadDatabase extends Database {
|
|||||||
String query = RECIPIENT_ID + " = ?";
|
String query = RECIPIENT_ID + " = ?";
|
||||||
|
|
||||||
for (Map.Entry<RecipientId, Boolean> entry : status.entrySet()) {
|
for (Map.Entry<RecipientId, Boolean> entry : status.entrySet()) {
|
||||||
ContentValues values = new ContentValues(1);
|
ContentValues values = new ContentValues(1);
|
||||||
|
|
||||||
|
if (entry.getValue()) {
|
||||||
|
values.put(PINNED, "0");
|
||||||
|
}
|
||||||
|
|
||||||
values.put(ARCHIVED, entry.getValue() ? "1" : "0");
|
values.put(ARCHIVED, entry.getValue() ? "1" : "0");
|
||||||
db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() });
|
db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() });
|
||||||
}
|
}
|
||||||
@ -584,14 +591,6 @@ public class ThreadDatabase extends Database {
|
|||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getPinnedConversationList(long offset, long limit) {
|
|
||||||
return getUnarchivedConversationList("1", offset, limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Cursor getUnpinnedConversationList(long offset, long limit) {
|
|
||||||
return getUnarchivedConversationList("0", offset, limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Cursor getArchivedConversationList(long offset, long limit) {
|
public Cursor getArchivedConversationList(long offset, long limit) {
|
||||||
return getConversationList("1", offset, limit);
|
return getConversationList("1", offset, limit);
|
||||||
}
|
}
|
||||||
@ -600,10 +599,10 @@ public class ThreadDatabase extends Database {
|
|||||||
return getConversationList(archived, 0, 0);
|
return getConversationList(archived, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cursor getUnarchivedConversationList(@NonNull String pinned, long offset, long limit) {
|
public Cursor getUnarchivedConversationList(boolean pinned, long offset, long limit) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String query = createQuery(ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + PINNED + " = ?", offset, limit);
|
String query = createQuery(ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + PINNED + " = ?", offset, limit);
|
||||||
Cursor cursor = db.rawQuery(query, new String[]{pinned});
|
Cursor cursor = db.rawQuery(query, new String[]{pinned ? "1" : "0"});
|
||||||
|
|
||||||
setNotifyConversationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
|
|
||||||
@ -620,14 +619,6 @@ public class ThreadDatabase extends Database {
|
|||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPinnedConversationListCount() {
|
|
||||||
return getUnarchivedConversationListCount(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getUnpinnedConversationListCount() {
|
|
||||||
return getUnarchivedConversationListCount(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getArchivedConversationListCount() {
|
public int getArchivedConversationListCount() {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String[] columns = new String[] { "COUNT(*)" };
|
String[] columns = new String[] { "COUNT(*)" };
|
||||||
@ -643,13 +634,26 @@ public class ThreadDatabase extends Database {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getUnarchivedConversationListCount(boolean pinned) {
|
public int getPinnedConversationListCount() {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String[] columns = new String[] { "COUNT(*)" };
|
String[] columns = new String[] { "COUNT(*)" };
|
||||||
String query = ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + PINNED + " = ?";
|
String query = ARCHIVED + " = 0 AND " + PINNED + " = 1 AND " + MESSAGE_COUNT + " != 0";
|
||||||
String[] args = new String[] { pinned ? "1" : "0" };
|
|
||||||
|
|
||||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null)) {
|
try (Cursor cursor = db.query(TABLE_NAME, columns, query, null, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
return cursor.getInt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUnarchivedConversationListCount() {
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
String[] columns = new String[] { "COUNT(*)" };
|
||||||
|
String query = ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0";
|
||||||
|
|
||||||
|
try (Cursor cursor = db.query(TABLE_NAME, columns, query, null, null, null, null)) {
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
return cursor.getInt(0);
|
return cursor.getInt(0);
|
||||||
}
|
}
|
||||||
@ -686,6 +690,7 @@ public class ThreadDatabase extends Database {
|
|||||||
public void archiveConversation(long threadId) {
|
public void archiveConversation(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
|
contentValues.put(PINNED, 0);
|
||||||
contentValues.put(ARCHIVED, 1);
|
contentValues.put(ARCHIVED, 1);
|
||||||
|
|
||||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||||
@ -1110,12 +1115,20 @@ public class ThreadDatabase extends Database {
|
|||||||
public static final int INBOX_ZERO = 4;
|
public static final int INBOX_ZERO = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Reader implements Closeable {
|
public class Reader extends StaticReader {
|
||||||
|
|
||||||
private final Cursor cursor;
|
|
||||||
|
|
||||||
public Reader(Cursor cursor) {
|
public Reader(Cursor cursor) {
|
||||||
this.cursor = cursor;
|
super(cursor, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StaticReader implements Closeable {
|
||||||
|
|
||||||
|
private final Cursor cursor;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public StaticReader(Cursor cursor, Context context) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ThreadRecord getNext() {
|
public ThreadRecord getNext() {
|
||||||
|
@ -0,0 +1,274 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversationlist;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||||
|
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||||
|
import org.powermock.modules.junit4.rule.PowerMockRule;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Matchers.anyLong;
|
||||||
|
import static org.mockito.Matchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||||
|
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||||
|
import static org.powermock.api.mockito.PowerMockito.when;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@Config(manifest = Config.NONE, application = Application.class)
|
||||||
|
@PowerMockIgnore({ "org.powermock.*", "org.mockito.*", "org.robolectric.*", "android.*", "androidx.*" })
|
||||||
|
@PrepareForTest({ApplicationDependencies.class, DatabaseFactory.class, ThreadDatabase.class})
|
||||||
|
public class UnarchivedConversationListDataSourceTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public PowerMockRule rule = new PowerMockRule();
|
||||||
|
|
||||||
|
private ConversationListDataSource.UnarchivedConversationListDataSource testSubject;
|
||||||
|
|
||||||
|
private ThreadDatabase threadDatabase;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
mockStatic(ApplicationDependencies.class);
|
||||||
|
mockStatic(DatabaseFactory.class);
|
||||||
|
|
||||||
|
final Context context = mock(Context.class);
|
||||||
|
final ContentResolver contentResolver = mock(ContentResolver.class);
|
||||||
|
threadDatabase = mock(ThreadDatabase.class);
|
||||||
|
|
||||||
|
when(DatabaseFactory.getThreadDatabase(any())).thenReturn(threadDatabase);
|
||||||
|
when(context.getContentResolver()).thenReturn(contentResolver);
|
||||||
|
|
||||||
|
testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(context, mock(Invalidator.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenNoConversations_whenIGetTotalCount_thenIExpectZero() {
|
||||||
|
// WHEN
|
||||||
|
int result = testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertEquals(0, result);
|
||||||
|
assertEquals(0, testSubject.getHeaderOffset());
|
||||||
|
assertFalse(testSubject.hasPinnedHeader());
|
||||||
|
assertFalse(testSubject.hasUnpinnedHeader());
|
||||||
|
assertFalse(testSubject.hasArchivedFooter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenArchivedConversations_whenIGetTotalCount_thenIExpectOne() {
|
||||||
|
// GIVEN
|
||||||
|
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
int result = testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertEquals(1, result);
|
||||||
|
assertEquals(0, testSubject.getHeaderOffset());
|
||||||
|
assertFalse(testSubject.hasPinnedHeader());
|
||||||
|
assertFalse(testSubject.hasUnpinnedHeader());
|
||||||
|
assertTrue(testSubject.hasArchivedFooter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenSinglePinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectThree() {
|
||||||
|
// GIVEN
|
||||||
|
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
int result = testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertEquals(3, result);
|
||||||
|
assertEquals(1, testSubject.getHeaderOffset());
|
||||||
|
assertTrue(testSubject.hasPinnedHeader());
|
||||||
|
assertFalse(testSubject.hasUnpinnedHeader());
|
||||||
|
assertTrue(testSubject.hasArchivedFooter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenSingleUnpinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectTwo() {
|
||||||
|
// GIVEN
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
int result = testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertEquals(2, result);
|
||||||
|
assertEquals(0, testSubject.getHeaderOffset());
|
||||||
|
assertFalse(testSubject.hasPinnedHeader());
|
||||||
|
assertFalse(testSubject.hasUnpinnedHeader());
|
||||||
|
assertTrue(testSubject.hasArchivedFooter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenSinglePinnedAndSingleUnpinned_whenIGetTotalCount_thenIExpectFour() {
|
||||||
|
// GIVEN
|
||||||
|
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(2);
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
int result = testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertEquals(4, result);
|
||||||
|
assertEquals(2, testSubject.getHeaderOffset());
|
||||||
|
assertTrue(testSubject.hasPinnedHeader());
|
||||||
|
assertTrue(testSubject.hasUnpinnedHeader());
|
||||||
|
assertFalse(testSubject.hasArchivedFooter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenNoConversations_whenIGetCursor_thenIExpectAnEmptyCursor() {
|
||||||
|
// GIVEN
|
||||||
|
setupThreadDatabaseCursors(0, 0);
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Cursor cursor = testSubject.getCursor(0, 100);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||||
|
assertEquals(0, cursor.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenArchivedConversations_whenIGetCursor_thenIExpectOne() {
|
||||||
|
// GIVEN
|
||||||
|
setupThreadDatabaseCursors(0, 0);
|
||||||
|
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||||
|
testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Cursor cursor = testSubject.getCursor(0, 100);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||||
|
assertEquals(1, cursor.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenSinglePinnedAndArchivedConversations_whenIGetCursor_thenIExpectThree() {
|
||||||
|
// GIVEN
|
||||||
|
setupThreadDatabaseCursors(1, 0);
|
||||||
|
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||||
|
testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Cursor cursor = testSubject.getCursor(0, 100);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(true, 0, 99);
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(false, 0, 98);
|
||||||
|
assertEquals(3, cursor.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenSingleUnpinnedAndArchivedConversations_whenIGetCursor_thenIExpectTwo() {
|
||||||
|
// GIVEN
|
||||||
|
setupThreadDatabaseCursors(0, 1);
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||||
|
testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Cursor cursor = testSubject.getCursor(0, 100);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||||
|
assertEquals(2, cursor.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenSinglePinnedAndSingleUnpinned_whenIGetCursor_thenIExpectFour() {
|
||||||
|
// GIVEN
|
||||||
|
setupThreadDatabaseCursors(1, 1);
|
||||||
|
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(2);
|
||||||
|
testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Cursor cursor = testSubject.getCursor(0, 100);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(true, 0, 99);
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(false, 0, 97);
|
||||||
|
assertEquals(4, cursor.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenLoadingSecondPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
|
||||||
|
// GIVEN
|
||||||
|
setupThreadDatabaseCursors(0, 100);
|
||||||
|
when(threadDatabase.getPinnedConversationListCount()).thenReturn(4);
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(104);
|
||||||
|
testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Cursor cursor = testSubject.getCursor(50, 100);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(true, 50, 100);
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(false, 44, 100);
|
||||||
|
assertEquals(100, cursor.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenHasArchivedAndLoadingLastPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
|
||||||
|
// GIVEN
|
||||||
|
setupThreadDatabaseCursors(0, 99);
|
||||||
|
when(threadDatabase.getPinnedConversationListCount()).thenReturn(4);
|
||||||
|
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(103);
|
||||||
|
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||||
|
testSubject.getTotalCount();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
Cursor cursor = testSubject.getCursor(50, 100);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(true, 50, 100);
|
||||||
|
verify(threadDatabase).getUnarchivedConversationList(false, 44, 100);
|
||||||
|
assertEquals(100, cursor.getCount());
|
||||||
|
|
||||||
|
cursor.moveToLast();
|
||||||
|
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setupThreadDatabaseCursors(int pinned, int unpinned) {
|
||||||
|
Cursor pinnedCursor = mock(Cursor.class);
|
||||||
|
when(pinnedCursor.getCount()).thenReturn(pinned);
|
||||||
|
|
||||||
|
Cursor unpinnedCursor = mock(Cursor.class);
|
||||||
|
when(unpinnedCursor.getCount()).thenReturn(unpinned);
|
||||||
|
|
||||||
|
when(threadDatabase.getUnarchivedConversationList(eq(true), anyLong(), anyLong())).thenReturn(pinnedCursor);
|
||||||
|
when(threadDatabase.getUnarchivedConversationList(eq(false), anyLong(), anyLong())).thenReturn(unpinnedCursor);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user