Add ability to pin up to 4 conversations.
This commit is contained in:
parent
9892c4392e
commit
d63e5165eb
18 changed files with 528 additions and 71 deletions
|
@ -0,0 +1,150 @@
|
|||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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.logging.Log;
|
||||
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(glideRequests, onConversationClickListener);
|
||||
this.unpinnedHeaderAdapter = new FixedViewsAdapter(unpinned);
|
||||
this.unpinnedAdapter = new ConversationListAdapter(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
|
||||
class ConversationListAdapter extends PagedListAdapter<Conversation, ConversationListAdapter.BaseViewHolder> {
|
||||
|
||||
private static final int TYPE_THREAD = 1;
|
||||
private static final int TYPE_ACTION = 2;
|
||||
|
@ -55,13 +55,13 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
public @NonNull BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
if (viewType == TYPE_ACTION) {
|
||||
ConversationViewHolder holder = new ConversationViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_action, parent, false));
|
||||
.inflate(R.layout.conversation_list_item_action, parent, false), viewType);
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int position = holder.getAdapterPosition();
|
||||
int position = holder.getLocalAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onConversationClickListener.onShowArchiveClick();
|
||||
|
@ -71,10 +71,10 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
return holder;
|
||||
} else if (viewType == TYPE_THREAD) {
|
||||
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false), viewType);
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int position = holder.getAdapterPosition();
|
||||
int position = holder.getLocalAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onConversationClickListener.onConversationClick(getItem(position));
|
||||
|
@ -82,7 +82,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
int position = holder.getAdapterPosition();
|
||||
int position = holder.getLocalAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
return onConversationClickListener.onConversationLongClick(getItem(position));
|
||||
|
@ -101,7 +101,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
|
@ -120,8 +120,9 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder.getItemViewType() == TYPE_ACTION) {
|
||||
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
|
||||
holder.setLocalAdapterPosition(position);
|
||||
if (holder.getLocalViewType() == TYPE_ACTION) {
|
||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
||||
|
||||
casted.getConversationListItem().bind(new ThreadRecord.Builder(100)
|
||||
|
@ -135,7 +136,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
typingSet,
|
||||
getBatchSelectionIds(),
|
||||
batchMode);
|
||||
} else if (holder.getItemViewType() == TYPE_THREAD) {
|
||||
} else if (holder.getLocalViewType() == TYPE_THREAD) {
|
||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||
|
||||
|
@ -149,7 +150,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
public void onViewRecycled(@NonNull BaseViewHolder holder) {
|
||||
if (holder instanceof ConversationViewHolder) {
|
||||
((ConversationViewHolder) holder).getConversationListItem().unbind();
|
||||
}
|
||||
|
@ -234,12 +235,35 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
static final class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||
static class BaseViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final int viewType;
|
||||
private int adapterPosition = RecyclerView.NO_POSITION;
|
||||
|
||||
public BaseViewHolder(@NonNull View itemView, int viewType) {
|
||||
super(itemView);
|
||||
this.viewType = viewType;
|
||||
}
|
||||
|
||||
public int getLocalViewType() {
|
||||
return viewType;
|
||||
}
|
||||
|
||||
public int getLocalAdapterPosition() {
|
||||
return getAdapterPosition() == RecyclerView.NO_POSITION ? RecyclerView.NO_POSITION : adapterPosition;
|
||||
}
|
||||
|
||||
public void setLocalAdapterPosition(int adapterPosition) {
|
||||
this.adapterPosition = adapterPosition;
|
||||
}
|
||||
}
|
||||
|
||||
static final class ConversationViewHolder extends BaseViewHolder {
|
||||
|
||||
private final BindableConversationListItem conversationListItem;
|
||||
|
||||
ConversationViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
ConversationViewHolder(@NonNull View itemView, int viewType) {
|
||||
super(itemView, viewType);
|
||||
|
||||
conversationListItem = (BindableConversationListItem) itemView;
|
||||
}
|
||||
|
@ -262,9 +286,9 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
|||
}
|
||||
}
|
||||
|
||||
private static class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
||||
private static class PlaceholderViewHolder extends BaseViewHolder {
|
||||
PlaceholderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
super(itemView, TYPE_PLACEHOLDER);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
|
||||
|
@ -58,9 +57,10 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
context.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, contentObserver);
|
||||
}
|
||||
|
||||
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
|
||||
else return new ArchivedConversationListDataSource(context, invalidator);
|
||||
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isPinned, boolean isArchived) {
|
||||
if (isPinned) return new PinnedConversationListDataSource(context, invalidator);
|
||||
else if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
|
||||
else return new ArchivedConversationListDataSource(context, invalidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -142,12 +142,29 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
return threadDatabase.getUnarchivedConversationListCount();
|
||||
return threadDatabase.getUnpinnedConversationListCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor(long offset, long limit) {
|
||||
return threadDatabase.getConversationList(offset, limit);
|
||||
return threadDatabase.getUnpinnedConversationList(offset, limit);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PinnedConversationListDataSource extends ConversationListDataSource {
|
||||
|
||||
protected PinnedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
||||
super(context, invalidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
return threadDatabase.getPinnedConversationListCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor(long offset, long limit) {
|
||||
return threadDatabase.getPinnedConversationList(offset, limit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,17 +172,19 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
|
||||
private final Context context;
|
||||
private final Invalidator invalidator;
|
||||
private final boolean isPinned;
|
||||
private final boolean isArchived;
|
||||
|
||||
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isPinned, boolean isArchived) {
|
||||
this.context = context;
|
||||
this.invalidator = invalidator;
|
||||
this.isPinned = isPinned;
|
||||
this.isArchived = isArchived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource<Integer, Conversation> create() {
|
||||
return ConversationListDataSource.create(context, invalidator, isArchived);
|
||||
return ConversationListDataSource.create(context, invalidator, isPinned, isArchived);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import androidx.lifecycle.DefaultLifecycleObserver;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -100,7 +101,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
|
@ -147,6 +147,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
private static final String TAG = Log.tag(ConversationListFragment.class);
|
||||
|
||||
private static final int MAXIMUM_PINNED_CONVERSATIONS = 4;
|
||||
|
||||
private static final int[] EMPTY_IMAGES = new int[] { R.drawable.empty_inbox_1,
|
||||
R.drawable.empty_inbox_2,
|
||||
R.drawable.empty_inbox_3,
|
||||
|
@ -166,7 +168,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
private View toolbarShadow;
|
||||
private ConversationListViewModel viewModel;
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private CompositeConversationListAdapter defaultAdapter;
|
||||
private ConversationListSearchAdapter searchAdapter;
|
||||
private StickyHeaderDecoration searchAdapterDecoration;
|
||||
private ViewGroup megaphoneContainer;
|
||||
|
@ -280,7 +282,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = requireActivity().getMenuInflater();
|
||||
|
@ -457,7 +458,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}
|
||||
|
||||
private void initializeListAdapters() {
|
||||
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
|
||||
defaultAdapter = new CompositeConversationListAdapter(list, GlideApp.with(this), this);
|
||||
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
|
||||
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
|
||||
|
||||
|
@ -502,7 +503,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
|
||||
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList);
|
||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitUnpinnedList);
|
||||
viewModel.getPinnedConversations().observe(getViewLifecycleOwner(), this::onSubmitPinnedList);
|
||||
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
||||
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||
@Override
|
||||
|
@ -740,6 +743,43 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
alert.show();
|
||||
}
|
||||
|
||||
private void handlePinAllSelected() {
|
||||
final Set<Long> toPin = new HashSet<>(Stream.of(defaultAdapter.getBatchSelection())
|
||||
.filterNot(conversation -> conversation.getThreadRecord().isPinned())
|
||||
.map(conversation -> conversation.getThreadRecord().getThreadId())
|
||||
.toList());
|
||||
|
||||
if (toPin.size() + defaultAdapter.getPinnedItemCount() > MAXIMUM_PINNED_CONVERSATIONS) {
|
||||
Snackbar.make(fab,
|
||||
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
|
||||
Snackbar.LENGTH_LONG)
|
||||
.setTextColor(Color.WHITE)
|
||||
.show();
|
||||
actionMode.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
||||
ThreadDatabase db = DatabaseFactory.getThreadDatabase(ApplicationDependencies.getApplication());
|
||||
|
||||
db.pinConversations(toPin);
|
||||
|
||||
return null;
|
||||
}, unused -> actionMode.finish());
|
||||
}
|
||||
|
||||
private void handleUnpinAllSelected() {
|
||||
final Set<Long> toPin = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
|
||||
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
||||
ThreadDatabase db = DatabaseFactory.getThreadDatabase(ApplicationDependencies.getApplication());
|
||||
|
||||
db.unpinConversations(toPin);
|
||||
|
||||
return null;
|
||||
}, unused -> actionMode.finish());
|
||||
}
|
||||
|
||||
private void handleSelectAllThreads() {
|
||||
defaultAdapter.selectAllThreads();
|
||||
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size()));
|
||||
|
@ -749,8 +789,19 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
||||
}
|
||||
|
||||
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
||||
if (conversationList.isEmpty()) {
|
||||
private void onSubmitPinnedList(@NonNull PagedList<Conversation> pinnedConversations) {
|
||||
defaultAdapter.submitPinnedList(pinnedConversations);
|
||||
}
|
||||
|
||||
private void onSubmitUnpinnedList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
||||
defaultAdapter.submitUnpinnedList(conversationList.getConversations());
|
||||
defaultAdapter.updateArchived(conversationList.getArchivedCount());
|
||||
|
||||
onPostSubmitList();
|
||||
}
|
||||
|
||||
private void updateEmptyState(boolean isConversationEmpty) {
|
||||
if (isConversationEmpty) {
|
||||
Log.i(TAG, "Received an empty data set.");
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.VISIBLE);
|
||||
|
@ -763,11 +814,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
fab.stopPulse();
|
||||
cameraFab.stopPulse();
|
||||
}
|
||||
|
||||
defaultAdapter.submitList(conversationList.getConversations());
|
||||
defaultAdapter.updateArchived(conversationList.getArchivedCount());
|
||||
|
||||
onPostSubmitList();
|
||||
}
|
||||
|
||||
protected void onPostSubmitList() {
|
||||
|
@ -791,11 +837,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
@Override
|
||||
public boolean onConversationLongClick(Conversation conversation) {
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
|
||||
|
||||
defaultAdapter.initializeBatchMode(true);
|
||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
||||
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -803,6 +849,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
|
||||
inflater.inflate(R.menu.conversation_list_batch_pin, menu);
|
||||
inflater.inflate(getActionModeMenuRes(), menu);
|
||||
inflater.inflate(R.menu.conversation_list_batch, menu);
|
||||
|
||||
|
@ -831,6 +878,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
switch (item.getItemId()) {
|
||||
case R.id.menu_select_all: handleSelectAllThreads(); return true;
|
||||
case R.id.menu_delete_selected: handleDeleteAllSelected(); return true;
|
||||
case R.id.menu_pin_selected: handlePinAllSelected(); return true;
|
||||
case R.id.menu_unpin_selected: handleUnpinAllSelected(); return true;
|
||||
case R.id.menu_archive_selected: handleArchiveAllSelected(); return true;
|
||||
case R.id.menu_mark_as_read: handleMarkSelectedAsRead(); return true;
|
||||
case R.id.menu_mark_as_unread: handleMarkSelectedAsUnread(); return true;
|
||||
|
@ -875,7 +924,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}
|
||||
|
||||
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 canPin = defaultAdapter.getPinnedItemCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
||||
|
||||
if (hasUnread) {
|
||||
menu.findItem(R.id.menu_mark_as_unread).setVisible(false);
|
||||
|
@ -884,6 +935,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
menu.findItem(R.id.menu_mark_as_unread).setVisible(true);
|
||||
menu.findItem(R.id.menu_mark_as_read).setVisible(false);
|
||||
}
|
||||
|
||||
if (!isArchived() && hasUnpinned && canPin) {
|
||||
menu.findItem(R.id.menu_pin_selected).setVisible(true);
|
||||
menu.findItem(R.id.menu_unpin_selected).setVisible(false);
|
||||
} else if (!isArchived() && !hasUnpinned) {
|
||||
menu.findItem(R.id.menu_pin_selected).setVisible(false);
|
||||
menu.findItem(R.id.menu_unpin_selected).setVisible(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_pin_selected).setVisible(false);
|
||||
menu.findItem(R.id.menu_unpin_selected).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected @IdRes int getToolbarRes() {
|
||||
|
|
|
@ -35,15 +35,16 @@ class ConversationListViewModel extends ViewModel {
|
|||
|
||||
private static final String TAG = Log.tag(ConversationListViewModel.class);
|
||||
|
||||
private final Application application;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final LiveData<ConversationList> conversationList;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer debouncer;
|
||||
private final ContentObserver observer;
|
||||
private final Invalidator invalidator;
|
||||
private final Application application;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final LiveData<PagedList<Conversation>> pinnedList;
|
||||
private final LiveData<ConversationList> conversationList;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer debouncer;
|
||||
private final ContentObserver observer;
|
||||
private final Invalidator invalidator;
|
||||
|
||||
private String lastQuery;
|
||||
|
||||
|
@ -64,7 +65,7 @@ class ConversationListViewModel extends ViewModel {
|
|||
}
|
||||
};
|
||||
|
||||
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
|
||||
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, false, isArchived);
|
||||
PagedList.Config config = new PagedList.Config.Builder()
|
||||
.setPageSize(15)
|
||||
.setInitialLoadSizeHint(30)
|
||||
|
@ -96,6 +97,26 @@ class ConversationListViewModel extends ViewModel {
|
|||
|
||||
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() {
|
||||
return LiveDataUtil.combineLatest(getPinnedConversations(),
|
||||
getConversationList(),
|
||||
(pinned, unpinned) -> pinned.isEmpty() && unpinned.isEmpty());
|
||||
}
|
||||
|
||||
@NonNull LiveData<PagedList<Conversation>> getPinnedConversations() {
|
||||
return pinnedList;
|
||||
}
|
||||
|
||||
@NonNull LiveData<SearchResult> getSearchResult() {
|
||||
|
|
|
@ -31,6 +31,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
|
@ -96,6 +97,7 @@ public class ThreadDatabase extends Database {
|
|||
public static final String LAST_SEEN = "last_seen";
|
||||
public static final String HAS_SENT = "has_sent";
|
||||
private static final String LAST_SCROLLED = "last_scrolled";
|
||||
private static final String PINNED = "pinned";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
DATE + " INTEGER DEFAULT 0, " +
|
||||
|
@ -118,16 +120,18 @@ public class ThreadDatabase extends Database {
|
|||
HAS_SENT + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
UNREAD_COUNT + " INTEGER DEFAULT 0, " +
|
||||
LAST_SCROLLED + " INTEGER DEFAULT 0);";
|
||||
LAST_SCROLLED + " INTEGER DEFAULT 0, " +
|
||||
PINNED + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
|
||||
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
|
||||
"CREATE INDEX IF NOT EXISTS thread_pinned_index ON " + TABLE_NAME + " (" + PINNED + ");",
|
||||
};
|
||||
|
||||
private static final String[] THREAD_PROJECTION = {
|
||||
ID, DATE, MESSAGE_COUNT, RECIPIENT_ID, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE,
|
||||
SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, LAST_SCROLLED
|
||||
SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, LAST_SCROLLED, PINNED
|
||||
};
|
||||
|
||||
private static final List<String> TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION)
|
||||
|
@ -579,8 +583,12 @@ public class ThreadDatabase extends Database {
|
|||
return positions;
|
||||
}
|
||||
|
||||
public Cursor getConversationList(long offset, long limit) {
|
||||
return getConversationList("0", offset, limit);
|
||||
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) {
|
||||
|
@ -591,6 +599,16 @@ public class ThreadDatabase extends Database {
|
|||
return getConversationList(archived, 0, 0);
|
||||
}
|
||||
|
||||
private Cursor getUnarchivedConversationList(@NonNull String pinned, long offset, long limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = createQuery(ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + PINNED + " = ?", offset, limit);
|
||||
Cursor cursor = db.rawQuery(query, new String[]{pinned});
|
||||
|
||||
setNotifyConversationListListeners(cursor);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private Cursor getConversationList(@NonNull String archived, long offset, long limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", offset, limit);
|
||||
|
@ -601,19 +619,19 @@ public class ThreadDatabase extends Database {
|
|||
return cursor;
|
||||
}
|
||||
|
||||
public int getUnarchivedConversationListCount() {
|
||||
return getConversationListCount(false);
|
||||
public int getPinnedConversationListCount() {
|
||||
return getUnarchivedConversationListCount(true);
|
||||
}
|
||||
|
||||
public int getUnpinnedConversationListCount() {
|
||||
return getUnarchivedConversationListCount(false);
|
||||
}
|
||||
|
||||
public int getArchivedConversationListCount() {
|
||||
return getConversationListCount(true);
|
||||
}
|
||||
|
||||
private int getConversationListCount(boolean archived) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] columns = new String[] { "COUNT(*)" };
|
||||
String query = ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0";
|
||||
String[] args = new String[] { archived ? "1" : "0" };
|
||||
String[] args = new String[] {"1"};
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
|
@ -624,6 +642,46 @@ public class ThreadDatabase extends Database {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private int getUnarchivedConversationListCount(boolean pinned) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] columns = new String[] { "COUNT(*)" };
|
||||
String query = ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + PINNED + " = ?";
|
||||
String[] args = new String[] { pinned ? "1" : "0" };
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void pinConversations(@NonNull Set<Long> threadIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
String placeholders = StringUtil.join(Stream.of(threadIds).map(unused -> "?").toList(), ",");
|
||||
String selection = ID + " IN (" + placeholders + ")";
|
||||
|
||||
contentValues.put(PINNED, 1);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, selection, SqlUtil.buildArgs(Stream.of(threadIds).toArray()));
|
||||
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void unpinConversations(@NonNull Set<Long> threadIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
String placeholders = StringUtil.join(Stream.of(threadIds).map(unused -> "?").toList(), ",");
|
||||
String selection = ID + " IN (" + placeholders + ")";
|
||||
|
||||
contentValues.put(PINNED, 0);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, selection, SqlUtil.buildArgs(Stream.of(threadIds).toArray()));
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void archiveConversation(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
|
@ -1122,6 +1180,7 @@ public class ThreadDatabase extends Database {
|
|||
.setCount(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)))
|
||||
.setUnreadCount(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT)))
|
||||
.setForcedUnread(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)) == ReadStatus.FORCED_UNREAD.serialize())
|
||||
.setPinned(CursorUtil.requireBoolean(cursor, ThreadDatabase.PINNED))
|
||||
.setExtra(extra)
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -141,8 +141,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int BORDERLESS = 66;
|
||||
private static final int REMAPPED_RECORDS = 67;
|
||||
private static final int MENTIONS = 68;
|
||||
private static final int PINNED_CONVERSATIONS = 69;
|
||||
|
||||
private static final int DATABASE_VERSION = 68;
|
||||
private static final int DATABASE_VERSION = 69;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -991,6 +992,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
db.execSQL("ALTER TABLE recipient ADD COLUMN mention_setting INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < PINNED_CONVERSATIONS) {
|
||||
db.execSQL("ALTER TABLE thread ADD COLUMN pinned INTEGER DEFAULT 0");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS thread_pinned_index ON thread (pinned)");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -55,6 +55,7 @@ public final class ThreadRecord {
|
|||
private final boolean archived;
|
||||
private final long expiresIn;
|
||||
private final long lastSeen;
|
||||
private final boolean isPinned;
|
||||
|
||||
private ThreadRecord(@NonNull Builder builder) {
|
||||
this.threadId = builder.threadId;
|
||||
|
@ -75,6 +76,7 @@ public final class ThreadRecord {
|
|||
this.archived = builder.archived;
|
||||
this.expiresIn = builder.expiresIn;
|
||||
this.lastSeen = builder.lastSeen;
|
||||
this.isPinned = builder.isPinned;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
|
@ -187,6 +189,10 @@ public final class ThreadRecord {
|
|||
else return true;
|
||||
}
|
||||
|
||||
public boolean isPinned() {
|
||||
return isPinned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -205,6 +211,7 @@ public final class ThreadRecord {
|
|||
archived == that.archived &&
|
||||
expiresIn == that.expiresIn &&
|
||||
lastSeen == that.lastSeen &&
|
||||
isPinned == that.isPinned &&
|
||||
body.equals(that.body) &&
|
||||
recipient.equals(that.recipient) &&
|
||||
Objects.equals(snippetUri, that.snippetUri) &&
|
||||
|
@ -231,7 +238,8 @@ public final class ThreadRecord {
|
|||
distributionType,
|
||||
archived,
|
||||
expiresIn,
|
||||
lastSeen);
|
||||
lastSeen,
|
||||
isPinned);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
@ -253,6 +261,7 @@ public final class ThreadRecord {
|
|||
private boolean archived;
|
||||
private long expiresIn;
|
||||
private long lastSeen;
|
||||
private boolean isPinned;
|
||||
|
||||
public Builder(long threadId) {
|
||||
this.threadId = threadId;
|
||||
|
@ -348,6 +357,11 @@ public final class ThreadRecord {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setPinned(boolean isPinned) {
|
||||
this.isPinned = isPinned;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ThreadRecord build() {
|
||||
if (distributionType == ThreadDatabase.DistributionTypes.CONVERSATION) {
|
||||
Preconditions.checkArgument(threadId > 0);
|
||||
|
|
|
@ -46,10 +46,10 @@ public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter<Recycle
|
|||
/** Observes a single sub adapter and maps the positions on the events to global positions. */
|
||||
private static class AdapterDataObserver extends RecyclerView.AdapterDataObserver {
|
||||
|
||||
private final RecyclerViewConcatenateAdapter mergeAdapter;
|
||||
private final RecyclerView.Adapter<RecyclerView.ViewHolder> adapter;
|
||||
private final RecyclerViewConcatenateAdapter mergeAdapter;
|
||||
private final RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter;
|
||||
|
||||
AdapterDataObserver(RecyclerViewConcatenateAdapter mergeAdapter, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
|
||||
AdapterDataObserver(RecyclerViewConcatenateAdapter mergeAdapter, RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
|
||||
this.mergeAdapter = mergeAdapter;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter<Recycle
|
|||
|
||||
private static class ChildAdapter {
|
||||
|
||||
final RecyclerView.Adapter<RecyclerView.ViewHolder> adapter;
|
||||
final RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter;
|
||||
|
||||
/** Map of global view types to local view types */
|
||||
private final SparseIntArray globalViewTypesMap = new SparseIntArray();
|
||||
|
@ -96,7 +96,7 @@ public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter<Recycle
|
|||
/** Map of local ids to global ids. */
|
||||
private final LongSparseArray<Long> localItemIdMap = new LongSparseArray<>();
|
||||
|
||||
ChildAdapter(@NonNull RecyclerView.Adapter<RecyclerView.ViewHolder> adapter, @NonNull AdapterDataObserver adapterDataObserver) {
|
||||
ChildAdapter(@NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter, @NonNull AdapterDataObserver adapterDataObserver) {
|
||||
this.adapter = adapter;
|
||||
this.adapterDataObserver = adapterDataObserver;
|
||||
|
||||
|
@ -153,7 +153,7 @@ public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter<Recycle
|
|||
localPosition = position;
|
||||
}
|
||||
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder> getAdapter() {
|
||||
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
|
||||
return childAdapter.adapter;
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter<Recycle
|
|||
/**
|
||||
* @param adapter Append an adapter to the list of adapters.
|
||||
*/
|
||||
public void addAdapter(@NonNull RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
|
||||
public void addAdapter(@NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
|
||||
addAdapter(adapters.size(), adapter);
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter<Recycle
|
|||
* @param index The index at which to add an adapter to the list of adapters.
|
||||
* @param adapter The adapter to add.
|
||||
*/
|
||||
public void addAdapter(int index, @NonNull RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
|
||||
public void addAdapter(int index, @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
|
||||
AdapterDataObserver adapterDataObserver = new AdapterDataObserver(this, adapter);
|
||||
adapters.add(index, new ChildAdapter(adapter, adapterDataObserver));
|
||||
notifyDataSetChanged();
|
||||
|
|
13
app/src/main/res/drawable/ic_pin_outline_24.xml
Normal file
13
app/src/main/res/drawable/ic_pin_outline_24.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M19.5223,13.9603C19.5223,12.3268 17.8418,10.9401 15.3681,10.2408L15.3681,7.3969C16.0561,7.1171 16.6526,6.6511 17.0906,6.0512C17.5286,5.4513 17.7908,4.7412 17.8477,4.0006C17.8477,3.084 15.2447,2.3377 12.0305,2.3436C8.8164,2.3495 6.2251,3.0722 6.2133,4.0006C6.2703,4.7412 6.5324,5.4513 6.9704,6.0512C7.4084,6.6511 8.0049,7.1171 8.693,7.3969L8.7165,10.2408C6.2545,10.9166 4.5622,12.3268 4.5622,13.9603C4.5622,15.5938 7.5824,16.7338 11.4194,16.8571L11.4194,22.7331L12.0423,23.9318L12.6651,22.6861L12.6651,16.8101C16.5021,16.7338 19.5223,15.4881 19.5223,13.9603ZM12.0423,15.6232C8.0466,15.6232 5.9372,14.4069 5.8079,13.9603C5.8138,13.0025 7.1359,11.9742 9.0514,11.4454L9.9681,11.1869L9.9857,6.5978L9.2982,6.2511C8.8704,6.0808 8.4878,5.814 8.18,5.4715C7.8722,5.1291 7.6476,4.7202 7.5237,4.2768C8.9716,3.725 10.5181,3.4789 12.0658,3.554C13.6135,3.4789 15.1599,3.725 16.6079,4.2768C16.484,4.7202 16.2594,5.1291 15.9516,5.4715C15.6438,5.814 15.2611,6.0808 14.8333,6.2511L14.0989,6.5978L14.0989,11.1692L15.0155,11.4278C16.9487,11.9742 18.2708,13.0025 18.2766,13.9368C18.1474,14.4069 16.0379,15.6232 12.0423,15.6232Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</group>
|
||||
</vector>
|
18
app/src/main/res/drawable/ic_pin_solid_24.xml
Normal file
18
app/src/main/res/drawable/ic_pin_solid_24.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M-0.95,10.94l12.9966,-12.9966l12.9966,12.9966l-12.9966,12.9966z"/>
|
||||
<path
|
||||
android:pathData="M18.9402,13.6639C18.9402,12.1584 17.3915,10.8804 15.1116,10.236L15.1116,7.615C15.7458,7.3572 16.2955,6.9277 16.6992,6.3748C17.1028,5.822 17.3444,5.1676 17.3969,4.485C17.3969,3.6402 14.9979,2.9525 12.0358,2.9579C9.0736,2.9633 6.6855,3.6294 6.6747,4.485C6.7271,5.1676 6.9688,5.822 7.3724,6.3748C7.7761,6.9277 8.3258,7.3572 8.9599,7.615L8.9816,10.236C6.7126,10.8588 5.153,12.1584 5.153,13.6639C5.153,15.1693 7.9364,16.2199 11.4726,16.3336L11.4726,21.7489L12.0466,22.8536L12.6206,21.7055L12.6206,16.2903C16.1568,16.2199 18.9402,15.0718 18.9402,13.6639Z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
17
app/src/main/res/drawable/ic_unpin_outline_24.xml
Normal file
17
app/src/main/res/drawable/ic_unpin_outline_24.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M17.8973,11.368C18.9291,12.0782 19.5428,12.9757 19.5428,13.9633C19.5428,15.4926 16.5195,16.7396 12.6787,16.816V22.6978L12.0552,23.9448L11.4318,22.7449L11.4318,16.8631C11.4051,16.8622 11.3784,16.8613 11.3518,16.8603L12.8397,15.6118C16.331,15.4668 18.1752,14.3786 18.2958,13.9398C18.2921,13.3517 17.7674,12.7264 16.8972,12.2072L17.8973,11.368ZM16.4669,10.6102C16.127,10.4716 15.765,10.3478 15.3843,10.2402V7.3934C16.0731,7.1133 16.6702,6.6468 17.1086,6.0463C17.5471,5.4458 17.8095,4.735 17.8665,3.9937C17.8665,3.0761 15.2608,2.3291 12.0435,2.335C8.8261,2.3409 6.2323,3.0644 6.2205,3.9937C6.2775,4.735 6.5399,5.4458 6.9783,6.0463C7.4168,6.6468 8.0139,7.1133 8.7026,7.3934L8.7261,10.2402C6.2617,10.9166 4.5677,12.3282 4.5677,13.9633C4.5677,15.2716 6.5029,16.2635 9.2437,16.6711L10.5586,15.5679C7.5286,15.3174 5.9268,14.3509 5.8147,13.9633C5.8205,13.0046 7.1439,11.9753 9.0614,11.4459L9.979,11.1871L9.9966,6.5935L9.3084,6.2464C8.8802,6.0759 8.4972,5.8089 8.1891,5.4661C7.881,5.1233 7.6562,4.714 7.5321,4.2701C8.9815,3.7178 10.5295,3.4715 12.0788,3.5467C13.628,3.4715 15.176,3.7178 16.6254,4.2701C16.5014,4.714 16.2766,5.1233 15.9685,5.4661C15.6604,5.8089 15.2773,6.0759 14.8491,6.2464L14.1139,6.5935V11.1695L15.0314,11.4283C15.1464,11.4608 15.2592,11.495 15.3698,11.5308L16.4669,10.6102Z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M3.9071,21.1491l-0.9642,-1.1491l19.1511,-16.0697l0.9642,1.1491z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</group>
|
||||
</vector>
|
17
app/src/main/res/drawable/ic_unpin_solid_24.xml
Normal file
17
app/src/main/res/drawable/ic_unpin_solid_24.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M17.7347,11.5045C18.4966,12.1176 18.9402,12.8582 18.9402,13.6639C18.9402,15.0719 16.1568,16.2199 12.6206,16.2903V21.7055L12.0466,22.8536L11.4726,21.7489L11.4726,16.759L17.7347,11.5045ZM16.3722,10.6896L9.8005,16.2039C7.0974,15.8626 5.153,14.9221 5.153,13.6639C5.153,12.1584 6.7126,10.8588 8.9816,10.236L8.9599,7.615C8.3258,7.3572 7.7761,6.9277 7.3724,6.3748C6.9688,5.822 6.7271,5.1676 6.6747,4.485C6.6855,3.6294 9.0736,2.9633 12.0358,2.9579C14.9979,2.9525 17.3969,3.6402 17.3969,4.485C17.3444,5.1676 17.1028,5.822 16.6992,6.3748C16.2955,6.9277 15.7458,7.3572 15.1116,7.615V10.236C15.5626,10.3635 15.9849,10.5157 16.3722,10.6896Z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M3.91,21.1491l-0.9642,-1.1491l19.1511,-16.0697l0.9642,1.1491z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</group>
|
||||
</vector>
|
12
app/src/main/res/layout/conversation_list_item_header.xml
Normal file
12
app/src/main/res/layout/conversation_list_item_header.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textColor="?title_text_color_secondary"
|
||||
tools:text="Chats" />
|
14
app/src/main/res/menu/conversation_list_batch_pin.xml
Normal file
14
app/src/main/res/menu/conversation_list_batch_pin.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/menu_pin_selected"
|
||||
android:icon="?menu_pin_icon"
|
||||
android:title="@string/conversation_list_batch__menu_unpin_selected"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/menu_unpin_selected"
|
||||
android:icon="?menu_unpin_icon"
|
||||
android:title="@string/conversation_list_batch__menu_unpin_selected"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -218,6 +218,8 @@
|
|||
<attr name="menu_multi_select_icon" format="reference" />
|
||||
<attr name="menu_archive_icon" format="reference" />
|
||||
<attr name="menu_edit_icon" format="reference" />
|
||||
<attr name="menu_pin_icon" format="reference" />
|
||||
<attr name="menu_unpin_icon" format="reference" />
|
||||
|
||||
<attr name="message_icon" format="reference" />
|
||||
<attr name="notifications_icon" format="reference" />
|
||||
|
|
|
@ -2076,6 +2076,8 @@
|
|||
|
||||
<!-- conversation_list_batch -->
|
||||
<string name="conversation_list_batch__menu_delete_selected">Delete selected</string>
|
||||
<string name="conversation_list_batch__menu_pin_selected">Pin selected</string>
|
||||
<string name="conversation_list_batch__menu_unpin_selected">Unpin selected</string>
|
||||
<string name="conversation_list_batch__menu_select_all">Select all</string>
|
||||
<string name="conversation_list_batch_archive__menu_archive_selected">Archive selected</string>
|
||||
<string name="conversation_list_batch_unarchive__menu_unarchive_selected">Unarchive selected</string>
|
||||
|
@ -2086,6 +2088,9 @@
|
|||
<!-- conversation_list -->
|
||||
<string name="conversation_list_settings_shortcut">Settings shortcut</string>
|
||||
<string name="conversation_list_search_description">Search</string>
|
||||
<string name="conversation_list__pinned">Pinned</string>
|
||||
<string name="conversation_list__chats">Chats</string>
|
||||
<string name="conversation_list__you_can_only_pin_up_to_d_chats">You can only pin up to %1$d chats</string>
|
||||
|
||||
<!-- conversation_list_item_view -->
|
||||
<string name="conversation_list_item_view__contact_photo_image">Contact Photo Image</string>
|
||||
|
|
|
@ -383,6 +383,8 @@
|
|||
<item name="menu_multi_select_icon">@drawable/ic_select_24</item>
|
||||
<item name="menu_archive_icon">@drawable/ic_archive_white_24dp</item>
|
||||
<item name="menu_edit_icon">@drawable/ic_compose_outline_tinted_24</item>
|
||||
<item name="menu_pin_icon">@drawable/ic_pin_outline_24</item>
|
||||
<item name="menu_unpin_icon">@drawable/ic_unpin_outline_24</item>
|
||||
|
||||
<item name="message_icon">@drawable/ic_message_outline_tinted_bitmap_24</item>
|
||||
<item name="notifications_icon">@drawable/ic_bell_outline_24</item>
|
||||
|
@ -696,6 +698,8 @@
|
|||
<item name="menu_multi_select_icon">@drawable/ic_select_24</item>
|
||||
<item name="menu_archive_icon">@drawable/ic_archive_white_24dp</item>
|
||||
<item name="menu_edit_icon">@drawable/ic_compose_solid_tinted_24</item>
|
||||
<item name="menu_pin_icon">@drawable/ic_pin_solid_24</item>
|
||||
<item name="menu_unpin_icon">@drawable/ic_unpin_solid_24</item>
|
||||
|
||||
<item name="message_icon">@drawable/ic_message_solid_tinted_bitmap_24</item>
|
||||
<item name="notifications_icon">@drawable/ic_bell_solid_24</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue