Ensure all conversations are loaded before selecting all.

They might not be loaded yet due to pagination.
This commit is contained in:
Rashad Sookram 2021-12-02 13:33:22 -05:00 committed by Greyson Parrelli
parent 2c5f57486c
commit 398fdd84b9
10 changed files with 196 additions and 126 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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) {

View file

@ -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) {
}

View file

@ -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) {
}

View file

@ -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));
}

View file

@ -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);

View file

@ -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
}
}

View file

@ -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 {