diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java index 784d45cd9e..0f6565a446 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java @@ -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 typingThreads, - @NonNull Set selectedThreads, boolean batchMode); + @NonNull ConversationSet selectedConversations); - void setBatchMode(boolean batchMode); + void setSelectedConversations(@NonNull ConversationSet conversations); void updateTypingIndicator(@NonNull Set typingThreads); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java index 1c0ab6073a..3629df398c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java @@ -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 batchSet = Collections.synchronizedMap(new LinkedHashMap<>()); - private boolean batchMode = false; - private final Set typingSet = new HashSet<>(); + private ConversationSet selectedConversations = new ConversationSet(); + private final Set typingSet = new HashSet<>(); private PagingController pagingController; @@ -62,8 +59,8 @@ class ConversationListAdapter extends ListAdapter { if (holder.getAdapterPosition() != RecyclerView.NO_POSITION) { @@ -73,8 +70,8 @@ class ConversationListAdapter extends ListAdapter { int position = holder.getAdapterPosition(); @@ -116,7 +113,7 @@ class ConversationListAdapter extends ListAdapter getBatchSelection() { - return batchSet.values(); - } - @Override public int getItemViewType(int position) { Conversation conversation = getItem(position); @@ -213,27 +200,6 @@ class ConversationListAdapter extends ListAdapter 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 8220b6bfd7..e81ab2d219 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -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 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 items = new ArrayList<>(); + Set 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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 7a26cdef5b..a839f99dcc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -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 selectedThreads; private Set 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 typingThreads, - @NonNull Set 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 typingThreads, - @NonNull Set 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAction.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAction.java index 5aa03e6c65..8ded68af33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAction.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAction.java @@ -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 typingThreads, - @NonNull Set 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) { } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemInboxZero.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemInboxZero.java index b5ce469640..7710542713 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemInboxZero.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemInboxZero.java @@ -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 typingThreads, - @NonNull Set selectedThreads, - boolean batchMode) + @NonNull ConversationSet selectedConversations) { } @Override - public void setBatchMode(boolean batchMode) { + public void setSelectedConversations(@NonNull ConversationSet conversations) { } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java index 4bfdf666d5..361f2ae504 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java @@ -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 eventListener.onConversationClicked(conversationResult)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java index 6607976c3c..2c2e005fc0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java @@ -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; - private final MutableLiveData searchResult; - private final PagedData pagedData; - private final LiveData 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; + private final MutableLiveData searchResult; + private final MutableLiveData selectedConversations; + private final Set internalSelection; + private final ConversationListDataSource conversationListDataSource; + private final PagedData pagedData; + private final LiveData 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 currentSelectedConversations() { + return internalSelection; + } + + @NonNull LiveData getSelectedConversations() { + return selectedConversations; + } + + void startSelection(@NonNull Conversation conversation) { + setSelection(Collections.singleton(conversation)); + } + + void endSelection() { + setSelection(Collections.emptySet()); + } + + void toggleConversationSelected(@NonNull Conversation conversation) { + Set newSelection = new HashSet<>(internalSelection); + if (newSelection.contains(conversation)) { + newSelection.remove(conversation); + } else { + newSelection.add(conversation); + } + + setSelection(newSelection); + } + + private void setSelection(@NonNull Collection 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/ConversationSet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/ConversationSet.kt new file mode 100644 index 0000000000..1385d82f04 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/ConversationSet.kt @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.conversationlist.model + +class ConversationSet @JvmOverloads constructor( + private val conversations: Set = emptySet() +) : Set by conversations { + + private val threadIds by lazy { + conversations.map { it.threadRecord.threadId } + } + + fun containsThreadId(id: Long): Boolean { + return id in threadIds + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 49f4b36f23..1da8c341e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -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 {