Ensure all conversations are loaded before selecting all.
They might not be loaded yet due to pagination.
This commit is contained in:
parent
2c5f57486c
commit
398fdd84b9
10 changed files with 196 additions and 126 deletions
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
|
@ -13,8 +15,8 @@ public interface BindableConversationListItem extends Unbindable {
|
|||
void bind(@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
@NonNull ConversationSet selectedConversations);
|
||||
|
||||
void setBatchMode(boolean batchMode);
|
||||
void setSelectedConversations(@NonNull ConversationSet conversations);
|
||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||
}
|
||||
|
|
|
@ -16,17 +16,15 @@ import org.signal.paging.PagingController;
|
|||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -44,9 +42,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
|
||||
private final GlideRequests glideRequests;
|
||||
private final OnConversationClickListener onConversationClickListener;
|
||||
private final Map<Long, Conversation> batchSet = Collections.synchronizedMap(new LinkedHashMap<>());
|
||||
private boolean batchMode = false;
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
private ConversationSet selectedConversations = new ConversationSet();
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
|
||||
private PagingController pagingController;
|
||||
|
||||
|
@ -62,8 +59,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
@Override
|
||||
public @NonNull RecyclerView.ViewHolder 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));
|
||||
ConversationViewHolder holder = new ConversationViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_action, parent, false));
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (holder.getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
|
@ -73,8 +70,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
|
||||
return holder;
|
||||
} else if (viewType == TYPE_THREAD) {
|
||||
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int position = holder.getAdapterPosition();
|
||||
|
@ -116,7 +113,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
Payload payload = (Payload) payloadObject;
|
||||
|
||||
if (payload == Payload.SELECTION) {
|
||||
((ConversationViewHolder) holder).getConversationListItem().setBatchMode(batchMode);
|
||||
((ConversationViewHolder) holder).getConversationListItem().setSelectedConversations(selectedConversations);
|
||||
} else {
|
||||
((ConversationViewHolder) holder).getConversationListItem().updateTypingIndicator(typingSet);
|
||||
}
|
||||
|
@ -135,8 +132,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
typingSet,
|
||||
getBatchSelectionIds(),
|
||||
batchMode);
|
||||
selectedConversations);
|
||||
} else if (holder.getItemViewType() == TYPE_HEADER) {
|
||||
HeaderViewHolder casted = (HeaderViewHolder) holder;
|
||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||
|
@ -180,20 +176,11 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
notifyItemRangeChanged(0, getItemCount(), Payload.TYPING_INDICATOR);
|
||||
}
|
||||
|
||||
void toggleConversationInBatchSet(@NonNull Conversation conversation) {
|
||||
if (batchSet.containsKey(conversation.getThreadRecord().getThreadId())) {
|
||||
batchSet.remove(conversation.getThreadRecord().getThreadId());
|
||||
} else if (conversation.getThreadRecord().getThreadId() != -1) {
|
||||
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
||||
}
|
||||
|
||||
void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||
selectedConversations = conversations;
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
Collection<Conversation> getBatchSelection() {
|
||||
return batchSet.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
Conversation conversation = getItem(position);
|
||||
|
@ -213,27 +200,6 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
}
|
||||
}
|
||||
|
||||
@NonNull Set<Long> getBatchSelectionIds() {
|
||||
return batchSet.keySet();
|
||||
}
|
||||
|
||||
void selectAllThreads() {
|
||||
for (int i = 0; i < super.getItemCount(); i++) {
|
||||
Conversation conversation = getItem(i);
|
||||
if (conversation != null && conversation.getThreadRecord().getThreadId() >= 0) {
|
||||
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
||||
}
|
||||
}
|
||||
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
void initializeBatchMode(boolean toggle) {
|
||||
this.batchMode = toggle;
|
||||
batchSet.clear();
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
static final class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final BindableConversationListItem conversationListItem;
|
||||
|
|
|
@ -57,14 +57,12 @@ import androidx.appcompat.view.ActionMode;
|
|||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.airbnb.lottie.SimpleColorFilter;
|
||||
import com.annimon.stream.Stream;
|
||||
|
@ -673,6 +671,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
};
|
||||
|
||||
viewModel.getUnreadPaymentsLiveData().observe(getViewLifecycleOwner(), this::onUnreadPaymentsChanged);
|
||||
|
||||
viewModel.getSelectedConversations().observe(getViewLifecycleOwner(), conversations -> {
|
||||
defaultAdapter.setSelectedConversations(conversations);
|
||||
updateMultiSelectState();
|
||||
});
|
||||
}
|
||||
|
||||
private void onConversationListChanged(@NonNull List<Conversation> conversations) {
|
||||
|
@ -987,11 +990,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
});
|
||||
}
|
||||
|
||||
private void handleSelectAllThreads() {
|
||||
defaultAdapter.selectAllThreads();
|
||||
updateMultiSelectState();
|
||||
}
|
||||
|
||||
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType) {
|
||||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
||||
}
|
||||
|
@ -1083,9 +1081,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
if (actionMode == null) {
|
||||
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
|
||||
} else {
|
||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
||||
viewModel.toggleConversationSelected(conversation);
|
||||
|
||||
if (defaultAdapter.getBatchSelectionIds().size() == 0) {
|
||||
if (viewModel.currentSelectedConversations().isEmpty()) {
|
||||
endActionModeIfActive();
|
||||
} else {
|
||||
updateMultiSelectState();
|
||||
|
@ -1127,8 +1125,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}
|
||||
|
||||
items.add(new ActionItem(R.drawable.ic_select_24, getString(R.string.ConversationListFragment_select), () -> {
|
||||
defaultAdapter.initializeBatchMode(true);
|
||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
||||
viewModel.startSelection(conversation);
|
||||
startActionMode();
|
||||
}));
|
||||
|
||||
|
@ -1173,7 +1170,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
defaultAdapter.initializeBatchMode(false);
|
||||
viewModel.endSelection();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor});
|
||||
|
@ -1207,10 +1204,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}
|
||||
|
||||
private void updateMultiSelectState() {
|
||||
int count = defaultAdapter.getBatchSelectionIds().size();
|
||||
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
||||
boolean hasUnpinned = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned());
|
||||
boolean hasUnmuted = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().getRecipient().live().get().isMuted());
|
||||
int count = viewModel.currentSelectedConversations().size();
|
||||
boolean hasUnread = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
||||
boolean hasUnpinned = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned());
|
||||
boolean hasUnmuted = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().getRecipient().live().get().isMuted());
|
||||
boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
||||
|
||||
if (actionMode != null) {
|
||||
|
@ -1219,34 +1216,39 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
List<ActionItem> items = new ArrayList<>();
|
||||
|
||||
Set<Long> selectionIds = viewModel.currentSelectedConversations()
|
||||
.stream()
|
||||
.map(conversation -> conversation.getThreadRecord().getThreadId())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (hasUnread) {
|
||||
items.add(new ActionItem(R.drawable.ic_read_24, getResources().getQuantityString(R.plurals.ConversationListFragment_read_plural, count), () -> handleMarkAsRead(defaultAdapter.getBatchSelectionIds())));
|
||||
items.add(new ActionItem(R.drawable.ic_read_24, getResources().getQuantityString(R.plurals.ConversationListFragment_read_plural, count), () -> handleMarkAsRead(selectionIds)));
|
||||
} else {
|
||||
items.add(new ActionItem(R.drawable.ic_unread_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unread_plural, count), () -> handleMarkAsUnread(defaultAdapter.getBatchSelectionIds())));
|
||||
items.add(new ActionItem(R.drawable.ic_unread_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unread_plural, count), () -> handleMarkAsUnread(selectionIds)));
|
||||
}
|
||||
|
||||
if (!isArchived() && hasUnpinned && canPin) {
|
||||
items.add(new ActionItem(R.drawable.ic_pin_24, getResources().getQuantityString(R.plurals.ConversationListFragment_pin_plural, count), () -> handlePin(defaultAdapter.getBatchSelection())));
|
||||
items.add(new ActionItem(R.drawable.ic_pin_24, getResources().getQuantityString(R.plurals.ConversationListFragment_pin_plural, count), () -> handlePin(viewModel.currentSelectedConversations())));
|
||||
} else if (!isArchived() && !hasUnpinned) {
|
||||
items.add(new ActionItem(R.drawable.ic_unpin_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unpin_plural, count), () -> handleUnpin(defaultAdapter.getBatchSelectionIds())));
|
||||
items.add(new ActionItem(R.drawable.ic_unpin_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unpin_plural, count), () -> handleUnpin(selectionIds)));
|
||||
}
|
||||
|
||||
if (isArchived()) {
|
||||
items.add(new ActionItem(R.drawable.ic_unarchive_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unarchive_plural, count), () -> handleArchive(defaultAdapter.getBatchSelectionIds(), true)));
|
||||
items.add(new ActionItem(R.drawable.ic_unarchive_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unarchive_plural, count), () -> handleArchive(selectionIds, true)));
|
||||
} else {
|
||||
items.add(new ActionItem(R.drawable.ic_archive_24, getResources().getQuantityString(R.plurals.ConversationListFragment_archive_plural, count), () -> handleArchive(defaultAdapter.getBatchSelectionIds(), true)));
|
||||
items.add(new ActionItem(R.drawable.ic_archive_24, getResources().getQuantityString(R.plurals.ConversationListFragment_archive_plural, count), () -> handleArchive(selectionIds, true)));
|
||||
}
|
||||
|
||||
items.add(new ActionItem(R.drawable.ic_delete_24, getResources().getQuantityString(R.plurals.ConversationListFragment_delete_plural, count), () -> handleDelete(defaultAdapter.getBatchSelectionIds())));
|
||||
items.add(new ActionItem(R.drawable.ic_delete_24, getResources().getQuantityString(R.plurals.ConversationListFragment_delete_plural, count), () -> handleDelete(selectionIds)));
|
||||
|
||||
|
||||
if (hasUnmuted) {
|
||||
items.add(new ActionItem(R.drawable.ic_mute_24, getResources().getQuantityString(R.plurals.ConversationListFragment_mute_plural, count), () -> handleMute(defaultAdapter.getBatchSelection())));
|
||||
items.add(new ActionItem(R.drawable.ic_mute_24, getResources().getQuantityString(R.plurals.ConversationListFragment_mute_plural, count), () -> handleMute(viewModel.currentSelectedConversations())));
|
||||
} else {
|
||||
items.add(new ActionItem(R.drawable.ic_unmute_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unmute_plural, count), () -> handleUnmute(defaultAdapter.getBatchSelection())));
|
||||
items.add(new ActionItem(R.drawable.ic_unmute_24, getResources().getQuantityString(R.plurals.ConversationListFragment_unmute_plural, count), () -> handleUnmute(viewModel.currentSelectedConversations())));
|
||||
}
|
||||
|
||||
items.add(new ActionItem(R.drawable.ic_select_24, getString(R.string.ConversationListFragment_select_all), this::handleSelectAllThreads));
|
||||
items.add(new ActionItem(R.drawable.ic_select_24, getString(R.string.ConversationListFragment_select_all), viewModel::onSelectAllClick));
|
||||
|
||||
bottomActionBar.setItems(items);
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
|||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
|
@ -95,7 +97,6 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||
|
||||
private Set<Long> selectedThreads;
|
||||
private Set<Long> typingThreads;
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
|
@ -162,25 +163,22 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode)
|
||||
@NonNull ConversationSet selectedConversations)
|
||||
{
|
||||
bindThread(thread, glideRequests, locale, typingThreads, selectedThreads, batchMode, null);
|
||||
bindThread(thread, glideRequests, locale, typingThreads, selectedConversations, null);
|
||||
}
|
||||
|
||||
public void bindThread(@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode,
|
||||
@NonNull ConversationSet selectedConversations,
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
observeRecipient(thread.getRecipient().live());
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = selectedThreads;
|
||||
this.threadId = thread.getThreadId();
|
||||
this.glideRequests = glideRequests;
|
||||
this.unreadCount = thread.getUnreadCount();
|
||||
|
@ -217,7 +215,7 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
}
|
||||
|
||||
setStatusIcons(thread);
|
||||
setBatchMode(batchMode);
|
||||
setSelectedConversations(selectedConversations);
|
||||
setBadgeFromRecipient(recipient.get());
|
||||
setUnreadIndicator(thread);
|
||||
this.contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||
|
@ -241,7 +239,6 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
this.highlightSubstring = highlightSubstring;
|
||||
|
@ -254,7 +251,7 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
deliveryStatusIndicator.setNone();
|
||||
alertView.setNone();
|
||||
|
||||
setBatchMode(false);
|
||||
setSelectedConversations(new ConversationSet());
|
||||
setBadgeFromRecipient(recipient.get());
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||
}
|
||||
|
@ -268,7 +265,6 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
this.highlightSubstring = highlightSubstring;
|
||||
|
@ -281,7 +277,7 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
deliveryStatusIndicator.setNone();
|
||||
alertView.setNone();
|
||||
|
||||
setBatchMode(false);
|
||||
setSelectedConversations(new ConversationSet());
|
||||
setBadgeFromRecipient(recipient.get());
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||
}
|
||||
|
@ -290,7 +286,7 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
public void unbind() {
|
||||
if (this.recipient != null) {
|
||||
observeRecipient(null);
|
||||
setBatchMode(false);
|
||||
setSelectedConversations(new ConversationSet());
|
||||
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
|
||||
}
|
||||
|
||||
|
@ -298,10 +294,10 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setBatchMode(boolean batchMode) {
|
||||
this.batchMode = batchMode;
|
||||
public void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||
this.batchMode = !conversations.isEmpty();
|
||||
|
||||
boolean selected = batchMode && selectedThreads.contains(thread.getThreadId());
|
||||
boolean selected = batchMode && conversations.containsThreadId(thread.getThreadId());
|
||||
setSelected(selected);
|
||||
|
||||
if (recipient != null) {
|
||||
|
|
|
@ -9,6 +9,8 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
|
@ -42,8 +44,7 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
|
|||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode)
|
||||
@NonNull ConversationSet selectedConversations)
|
||||
{
|
||||
this.description.setText(getContext().getString(R.string.ConversationListItemAction_archived_conversations_d, thread.getUnreadCount()));
|
||||
}
|
||||
|
@ -54,7 +55,7 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setBatchMode(boolean batchMode) {
|
||||
public void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
|
@ -45,14 +47,13 @@ public class ConversationListItemInboxZero extends LinearLayout implements Binda
|
|||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode)
|
||||
@NonNull ConversationSet selectedConversations)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBatchMode(boolean batchMode) {
|
||||
public void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.search.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
|
@ -162,7 +163,7 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
|
|||
@NonNull Locale locale,
|
||||
@Nullable String query)
|
||||
{
|
||||
root.bindThread(conversationResult, glideRequests, locale, Collections.emptySet(), Collections.emptySet(), false, query);
|
||||
root.bindThread(conversationResult, glideRequests, locale, Collections.emptySet(), new ConversationSet(), query);
|
||||
root.setOnClickListener(view -> eventListener.onConversationClicked(conversationResult));
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.signal.paging.PagedData;
|
|||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
|
@ -33,9 +34,17 @@ import org.thoughtcrime.securesms.util.paging.Invalidator;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
class ConversationListViewModel extends ViewModel {
|
||||
|
||||
|
@ -43,42 +52,50 @@ class ConversationListViewModel extends ViewModel {
|
|||
|
||||
private static boolean coldStart = true;
|
||||
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final PagedData<Long, Conversation> pagedData;
|
||||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer messageSearchDebouncer;
|
||||
private final Debouncer contactSearchDebouncer;
|
||||
private final ThrottledDebouncer updateDebouncer;
|
||||
private final DatabaseObserver.Observer observer;
|
||||
private final Invalidator invalidator;
|
||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final MutableLiveData<ConversationSet> selectedConversations;
|
||||
private final Set<Conversation> internalSelection;
|
||||
private final ConversationListDataSource conversationListDataSource;
|
||||
private final PagedData<Long, Conversation> pagedData;
|
||||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer messageSearchDebouncer;
|
||||
private final Debouncer contactSearchDebouncer;
|
||||
private final ThrottledDebouncer updateDebouncer;
|
||||
private final DatabaseObserver.Observer observer;
|
||||
private final Invalidator invalidator;
|
||||
private final CompositeDisposable disposables;
|
||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||
|
||||
private String activeQuery;
|
||||
private SearchResult activeSearchResult;
|
||||
private int pinnedCount;
|
||||
|
||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
this.searchResult = new MutableLiveData<>();
|
||||
this.searchRepository = searchRepository;
|
||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
||||
this.messageSearchDebouncer = new Debouncer(500);
|
||||
this.contactSearchDebouncer = new Debouncer(100);
|
||||
this.updateDebouncer = new ThrottledDebouncer(500);
|
||||
this.activeSearchResult = SearchResult.EMPTY;
|
||||
this.invalidator = new Invalidator();
|
||||
this.pagedData = PagedData.create(ConversationListDataSource.create(application, isArchived),
|
||||
new PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build());
|
||||
this.unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
||||
this.observer = () -> {
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
this.searchResult = new MutableLiveData<>();
|
||||
this.internalSelection = new HashSet<>();
|
||||
this.selectedConversations = new MutableLiveData<>(new ConversationSet());
|
||||
this.searchRepository = searchRepository;
|
||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
||||
this.messageSearchDebouncer = new Debouncer(500);
|
||||
this.contactSearchDebouncer = new Debouncer(100);
|
||||
this.updateDebouncer = new ThrottledDebouncer(500);
|
||||
this.activeSearchResult = SearchResult.EMPTY;
|
||||
this.invalidator = new Invalidator();
|
||||
this.disposables = new CompositeDisposable();
|
||||
this.conversationListDataSource = ConversationListDataSource.create(application, isArchived);
|
||||
this.pagedData = PagedData.create(conversationListDataSource,
|
||||
new PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build());
|
||||
this.unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
||||
this.observer = () -> {
|
||||
updateDebouncer.publish(() -> {
|
||||
if (!TextUtils.isEmpty(activeQuery)) {
|
||||
onSearchQueryUpdated(activeQuery);
|
||||
|
@ -142,6 +159,48 @@ class ConversationListViewModel extends ViewModel {
|
|||
coldStart = false;
|
||||
}
|
||||
|
||||
@NonNull Set<Conversation> currentSelectedConversations() {
|
||||
return internalSelection;
|
||||
}
|
||||
|
||||
@NonNull LiveData<ConversationSet> getSelectedConversations() {
|
||||
return selectedConversations;
|
||||
}
|
||||
|
||||
void startSelection(@NonNull Conversation conversation) {
|
||||
setSelection(Collections.singleton(conversation));
|
||||
}
|
||||
|
||||
void endSelection() {
|
||||
setSelection(Collections.emptySet());
|
||||
}
|
||||
|
||||
void toggleConversationSelected(@NonNull Conversation conversation) {
|
||||
Set<Conversation> newSelection = new HashSet<>(internalSelection);
|
||||
if (newSelection.contains(conversation)) {
|
||||
newSelection.remove(conversation);
|
||||
} else {
|
||||
newSelection.add(conversation);
|
||||
}
|
||||
|
||||
setSelection(newSelection);
|
||||
}
|
||||
|
||||
private void setSelection(@NonNull Collection<Conversation> newSelection) {
|
||||
internalSelection.clear();
|
||||
internalSelection.addAll(newSelection);
|
||||
selectedConversations.setValue(new ConversationSet(internalSelection));
|
||||
}
|
||||
|
||||
void onSelectAllClick() {
|
||||
disposables.add(
|
||||
Single.fromCallable(() -> conversationListDataSource.load(0, conversationListDataSource.size(), disposables::isDisposed))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::setSelection)
|
||||
);
|
||||
}
|
||||
|
||||
void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
|
||||
megaphone.postValue(null);
|
||||
megaphoneRepository.markFinished(event);
|
||||
|
@ -210,6 +269,7 @@ class ConversationListViewModel extends ViewModel {
|
|||
@Override
|
||||
protected void onCleared() {
|
||||
invalidator.invalidate();
|
||||
disposables.dispose();
|
||||
messageSearchDebouncer.clear();
|
||||
updateDebouncer.clear();
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.conversationlist.model
|
||||
|
||||
class ConversationSet @JvmOverloads constructor(
|
||||
private val conversations: Set<Conversation> = emptySet()
|
||||
) : Set<Conversation> by conversations {
|
||||
|
||||
private val threadIds by lazy {
|
||||
conversations.map { it.threadRecord.threadId }
|
||||
}
|
||||
|
||||
fun containsThreadId(id: Long): Boolean {
|
||||
return id in threadIds
|
||||
}
|
||||
}
|
|
@ -1744,6 +1744,33 @@ public class ThreadDatabase extends Database {
|
|||
public @Nullable String getIndividualRecipientId() {
|
||||
return individualRecipientId;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Extra extra = (Extra) o;
|
||||
return isRevealable == extra.isRevealable &&
|
||||
isSticker == extra.isSticker &&
|
||||
isAlbum == extra.isAlbum &&
|
||||
isRemoteDelete == extra.isRemoteDelete &&
|
||||
isMessageRequestAccepted == extra.isMessageRequestAccepted &&
|
||||
isGv2Invite == extra.isGv2Invite &&
|
||||
Objects.equals(stickerEmoji, extra.stickerEmoji) &&
|
||||
Objects.equals(groupAddedBy, extra.groupAddedBy) &&
|
||||
Objects.equals(individualRecipientId, extra.individualRecipientId);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Objects.hash(isRevealable,
|
||||
isSticker,
|
||||
stickerEmoji,
|
||||
isAlbum,
|
||||
isRemoteDelete,
|
||||
isMessageRequestAccepted,
|
||||
isGv2Invite,
|
||||
groupAddedBy,
|
||||
individualRecipientId);
|
||||
}
|
||||
}
|
||||
|
||||
enum ReadStatus {
|
||||
|
|
Loading…
Add table
Reference in a new issue