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 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.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
|
||||||
|
@ -13,8 +15,8 @@ public interface BindableConversationListItem extends Unbindable {
|
||||||
void bind(@NonNull ThreadRecord thread,
|
void bind(@NonNull ThreadRecord thread,
|
||||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||||
@NonNull Set<Long> typingThreads,
|
@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);
|
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,17 +16,15 @@ import org.signal.paging.PagingController;
|
||||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -44,8 +42,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
private final GlideRequests glideRequests;
|
||||||
private final OnConversationClickListener onConversationClickListener;
|
private final OnConversationClickListener onConversationClickListener;
|
||||||
private final Map<Long, Conversation> batchSet = Collections.synchronizedMap(new LinkedHashMap<>());
|
private ConversationSet selectedConversations = new ConversationSet();
|
||||||
private boolean batchMode = false;
|
|
||||||
private final Set<Long> typingSet = new HashSet<>();
|
private final Set<Long> typingSet = new HashSet<>();
|
||||||
|
|
||||||
private PagingController pagingController;
|
private PagingController pagingController;
|
||||||
|
@ -116,7 +113,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||||
Payload payload = (Payload) payloadObject;
|
Payload payload = (Payload) payloadObject;
|
||||||
|
|
||||||
if (payload == Payload.SELECTION) {
|
if (payload == Payload.SELECTION) {
|
||||||
((ConversationViewHolder) holder).getConversationListItem().setBatchMode(batchMode);
|
((ConversationViewHolder) holder).getConversationListItem().setSelectedConversations(selectedConversations);
|
||||||
} else {
|
} else {
|
||||||
((ConversationViewHolder) holder).getConversationListItem().updateTypingIndicator(typingSet);
|
((ConversationViewHolder) holder).getConversationListItem().updateTypingIndicator(typingSet);
|
||||||
}
|
}
|
||||||
|
@ -135,8 +132,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||||
glideRequests,
|
glideRequests,
|
||||||
Locale.getDefault(),
|
Locale.getDefault(),
|
||||||
typingSet,
|
typingSet,
|
||||||
getBatchSelectionIds(),
|
selectedConversations);
|
||||||
batchMode);
|
|
||||||
} else if (holder.getItemViewType() == TYPE_HEADER) {
|
} else if (holder.getItemViewType() == TYPE_HEADER) {
|
||||||
HeaderViewHolder casted = (HeaderViewHolder) holder;
|
HeaderViewHolder casted = (HeaderViewHolder) holder;
|
||||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||||
|
@ -180,20 +176,11 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||||
notifyItemRangeChanged(0, getItemCount(), Payload.TYPING_INDICATOR);
|
notifyItemRangeChanged(0, getItemCount(), Payload.TYPING_INDICATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleConversationInBatchSet(@NonNull Conversation conversation) {
|
void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||||
if (batchSet.containsKey(conversation.getThreadRecord().getThreadId())) {
|
selectedConversations = conversations;
|
||||||
batchSet.remove(conversation.getThreadRecord().getThreadId());
|
|
||||||
} else if (conversation.getThreadRecord().getThreadId() != -1) {
|
|
||||||
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection<Conversation> getBatchSelection() {
|
|
||||||
return batchSet.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
Conversation conversation = getItem(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 {
|
static final class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private final BindableConversationListItem conversationListItem;
|
private final BindableConversationListItem conversationListItem;
|
||||||
|
|
|
@ -57,14 +57,12 @@ import androidx.appcompat.view.ActionMode;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.appcompat.widget.TooltipCompat;
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.constraintlayout.widget.ConstraintSet;
|
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.transition.TransitionManager;
|
|
||||||
|
|
||||||
import com.airbnb.lottie.SimpleColorFilter;
|
import com.airbnb.lottie.SimpleColorFilter;
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
@ -673,6 +671,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
};
|
};
|
||||||
|
|
||||||
viewModel.getUnreadPaymentsLiveData().observe(getViewLifecycleOwner(), this::onUnreadPaymentsChanged);
|
viewModel.getUnreadPaymentsLiveData().observe(getViewLifecycleOwner(), this::onUnreadPaymentsChanged);
|
||||||
|
|
||||||
|
viewModel.getSelectedConversations().observe(getViewLifecycleOwner(), conversations -> {
|
||||||
|
defaultAdapter.setSelectedConversations(conversations);
|
||||||
|
updateMultiSelectState();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onConversationListChanged(@NonNull List<Conversation> conversations) {
|
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) {
|
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType) {
|
||||||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
||||||
}
|
}
|
||||||
|
@ -1083,9 +1081,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
if (actionMode == null) {
|
if (actionMode == null) {
|
||||||
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
|
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
|
||||||
} else {
|
} else {
|
||||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
viewModel.toggleConversationSelected(conversation);
|
||||||
|
|
||||||
if (defaultAdapter.getBatchSelectionIds().size() == 0) {
|
if (viewModel.currentSelectedConversations().isEmpty()) {
|
||||||
endActionModeIfActive();
|
endActionModeIfActive();
|
||||||
} else {
|
} else {
|
||||||
updateMultiSelectState();
|
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), () -> {
|
items.add(new ActionItem(R.drawable.ic_select_24, getString(R.string.ConversationListFragment_select), () -> {
|
||||||
defaultAdapter.initializeBatchMode(true);
|
viewModel.startSelection(conversation);
|
||||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
|
||||||
startActionMode();
|
startActionMode();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -1173,7 +1170,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyActionMode(ActionMode mode) {
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
defaultAdapter.initializeBatchMode(false);
|
viewModel.endSelection();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor});
|
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() {
|
private void updateMultiSelectState() {
|
||||||
int count = defaultAdapter.getBatchSelectionIds().size();
|
int count = viewModel.currentSelectedConversations().size();
|
||||||
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
boolean hasUnread = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
||||||
boolean hasUnpinned = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned());
|
boolean hasUnpinned = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned());
|
||||||
boolean hasUnmuted = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().getRecipient().live().get().isMuted());
|
boolean hasUnmuted = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().getRecipient().live().get().isMuted());
|
||||||
boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
||||||
|
|
||||||
if (actionMode != null) {
|
if (actionMode != null) {
|
||||||
|
@ -1219,34 +1216,39 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
|
|
||||||
List<ActionItem> items = new ArrayList<>();
|
List<ActionItem> items = new ArrayList<>();
|
||||||
|
|
||||||
|
Set<Long> selectionIds = viewModel.currentSelectedConversations()
|
||||||
|
.stream()
|
||||||
|
.map(conversation -> conversation.getThreadRecord().getThreadId())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
if (hasUnread) {
|
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 {
|
} 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) {
|
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) {
|
} 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()) {
|
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 {
|
} 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) {
|
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 {
|
} 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);
|
bottomActionBar.setItems(items);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,8 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||||
import org.thoughtcrime.securesms.components.FromTextView;
|
import org.thoughtcrime.securesms.components.FromTextView;
|
||||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
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.MmsSmsColumns;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
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 BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||||
|
|
||||||
private Set<Long> selectedThreads;
|
|
||||||
private Set<Long> typingThreads;
|
private Set<Long> typingThreads;
|
||||||
private LiveRecipient recipient;
|
private LiveRecipient recipient;
|
||||||
private long threadId;
|
private long threadId;
|
||||||
|
@ -162,25 +163,22 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
@NonNull GlideRequests glideRequests,
|
@NonNull GlideRequests glideRequests,
|
||||||
@NonNull Locale locale,
|
@NonNull Locale locale,
|
||||||
@NonNull Set<Long> typingThreads,
|
@NonNull Set<Long> typingThreads,
|
||||||
@NonNull Set<Long> selectedThreads,
|
@NonNull ConversationSet selectedConversations)
|
||||||
boolean batchMode)
|
|
||||||
{
|
{
|
||||||
bindThread(thread, glideRequests, locale, typingThreads, selectedThreads, batchMode, null);
|
bindThread(thread, glideRequests, locale, typingThreads, selectedConversations, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void bindThread(@NonNull ThreadRecord thread,
|
public void bindThread(@NonNull ThreadRecord thread,
|
||||||
@NonNull GlideRequests glideRequests,
|
@NonNull GlideRequests glideRequests,
|
||||||
@NonNull Locale locale,
|
@NonNull Locale locale,
|
||||||
@NonNull Set<Long> typingThreads,
|
@NonNull Set<Long> typingThreads,
|
||||||
@NonNull Set<Long> selectedThreads,
|
@NonNull ConversationSet selectedConversations,
|
||||||
boolean batchMode,
|
|
||||||
@Nullable String highlightSubstring)
|
@Nullable String highlightSubstring)
|
||||||
{
|
{
|
||||||
observeRecipient(thread.getRecipient().live());
|
observeRecipient(thread.getRecipient().live());
|
||||||
observeDisplayBody(null);
|
observeDisplayBody(null);
|
||||||
setSubjectViewText(null);
|
setSubjectViewText(null);
|
||||||
|
|
||||||
this.selectedThreads = selectedThreads;
|
|
||||||
this.threadId = thread.getThreadId();
|
this.threadId = thread.getThreadId();
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.unreadCount = thread.getUnreadCount();
|
this.unreadCount = thread.getUnreadCount();
|
||||||
|
@ -217,7 +215,7 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusIcons(thread);
|
setStatusIcons(thread);
|
||||||
setBatchMode(batchMode);
|
setSelectedConversations(selectedConversations);
|
||||||
setBadgeFromRecipient(recipient.get());
|
setBadgeFromRecipient(recipient.get());
|
||||||
setUnreadIndicator(thread);
|
setUnreadIndicator(thread);
|
||||||
this.contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
this.contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||||
|
@ -241,7 +239,6 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
observeDisplayBody(null);
|
observeDisplayBody(null);
|
||||||
setSubjectViewText(null);
|
setSubjectViewText(null);
|
||||||
|
|
||||||
this.selectedThreads = Collections.emptySet();
|
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
this.highlightSubstring = highlightSubstring;
|
this.highlightSubstring = highlightSubstring;
|
||||||
|
@ -254,7 +251,7 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
deliveryStatusIndicator.setNone();
|
deliveryStatusIndicator.setNone();
|
||||||
alertView.setNone();
|
alertView.setNone();
|
||||||
|
|
||||||
setBatchMode(false);
|
setSelectedConversations(new ConversationSet());
|
||||||
setBadgeFromRecipient(recipient.get());
|
setBadgeFromRecipient(recipient.get());
|
||||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||||
}
|
}
|
||||||
|
@ -268,7 +265,6 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
observeDisplayBody(null);
|
observeDisplayBody(null);
|
||||||
setSubjectViewText(null);
|
setSubjectViewText(null);
|
||||||
|
|
||||||
this.selectedThreads = Collections.emptySet();
|
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
this.highlightSubstring = highlightSubstring;
|
this.highlightSubstring = highlightSubstring;
|
||||||
|
@ -281,7 +277,7 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
deliveryStatusIndicator.setNone();
|
deliveryStatusIndicator.setNone();
|
||||||
alertView.setNone();
|
alertView.setNone();
|
||||||
|
|
||||||
setBatchMode(false);
|
setSelectedConversations(new ConversationSet());
|
||||||
setBadgeFromRecipient(recipient.get());
|
setBadgeFromRecipient(recipient.get());
|
||||||
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
|
||||||
}
|
}
|
||||||
|
@ -290,7 +286,7 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
public void unbind() {
|
public void unbind() {
|
||||||
if (this.recipient != null) {
|
if (this.recipient != null) {
|
||||||
observeRecipient(null);
|
observeRecipient(null);
|
||||||
setBatchMode(false);
|
setSelectedConversations(new ConversationSet());
|
||||||
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
|
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,10 +294,10 @@ public final class ConversationListItem extends ConstraintLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBatchMode(boolean batchMode) {
|
public void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||||
this.batchMode = batchMode;
|
this.batchMode = !conversations.isEmpty();
|
||||||
|
|
||||||
boolean selected = batchMode && selectedThreads.contains(thread.getThreadId());
|
boolean selected = batchMode && conversations.containsThreadId(thread.getThreadId());
|
||||||
setSelected(selected);
|
setSelected(selected);
|
||||||
|
|
||||||
if (recipient != null) {
|
if (recipient != null) {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
|
||||||
|
@ -42,8 +44,7 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
|
||||||
@NonNull GlideRequests glideRequests,
|
@NonNull GlideRequests glideRequests,
|
||||||
@NonNull Locale locale,
|
@NonNull Locale locale,
|
||||||
@NonNull Set<Long> typingThreads,
|
@NonNull Set<Long> typingThreads,
|
||||||
@NonNull Set<Long> selectedThreads,
|
@NonNull ConversationSet selectedConversations)
|
||||||
boolean batchMode)
|
|
||||||
{
|
{
|
||||||
this.description.setText(getContext().getString(R.string.ConversationListItemAction_archived_conversations_d, thread.getUnreadCount()));
|
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
|
@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 androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
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.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
|
||||||
|
@ -45,14 +47,13 @@ public class ConversationListItemInboxZero extends LinearLayout implements Binda
|
||||||
@NonNull GlideRequests glideRequests,
|
@NonNull GlideRequests glideRequests,
|
||||||
@NonNull Locale locale,
|
@NonNull Locale locale,
|
||||||
@NonNull Set<Long> typingThreads,
|
@NonNull Set<Long> typingThreads,
|
||||||
@NonNull Set<Long> selectedThreads,
|
@NonNull ConversationSet selectedConversations)
|
||||||
boolean batchMode)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||||
import org.thoughtcrime.securesms.search.MessageResult;
|
import org.thoughtcrime.securesms.search.MessageResult;
|
||||||
import org.thoughtcrime.securesms.search.SearchResult;
|
import org.thoughtcrime.securesms.search.SearchResult;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
|
@ -162,7 +163,7 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
|
||||||
@NonNull Locale locale,
|
@NonNull Locale locale,
|
||||||
@Nullable String query)
|
@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));
|
root.setOnClickListener(view -> eventListener.onConversationClicked(conversationResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import org.signal.paging.PagedData;
|
||||||
import org.signal.paging.PagingConfig;
|
import org.signal.paging.PagingConfig;
|
||||||
import org.signal.paging.PagingController;
|
import org.signal.paging.PagingController;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
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.UnreadPayments;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
|
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
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.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
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.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
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 {
|
class ConversationListViewModel extends ViewModel {
|
||||||
|
|
||||||
|
@ -45,6 +54,9 @@ class ConversationListViewModel extends ViewModel {
|
||||||
|
|
||||||
private final MutableLiveData<Megaphone> megaphone;
|
private final MutableLiveData<Megaphone> megaphone;
|
||||||
private final MutableLiveData<SearchResult> searchResult;
|
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 PagedData<Long, Conversation> pagedData;
|
||||||
private final LiveData<Boolean> hasNoConversations;
|
private final LiveData<Boolean> hasNoConversations;
|
||||||
private final SearchRepository searchRepository;
|
private final SearchRepository searchRepository;
|
||||||
|
@ -54,6 +66,7 @@ class ConversationListViewModel extends ViewModel {
|
||||||
private final ThrottledDebouncer updateDebouncer;
|
private final ThrottledDebouncer updateDebouncer;
|
||||||
private final DatabaseObserver.Observer observer;
|
private final DatabaseObserver.Observer observer;
|
||||||
private final Invalidator invalidator;
|
private final Invalidator invalidator;
|
||||||
|
private final CompositeDisposable disposables;
|
||||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||||
|
|
||||||
|
@ -64,6 +77,8 @@ class ConversationListViewModel extends ViewModel {
|
||||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
||||||
this.megaphone = new MutableLiveData<>();
|
this.megaphone = new MutableLiveData<>();
|
||||||
this.searchResult = new MutableLiveData<>();
|
this.searchResult = new MutableLiveData<>();
|
||||||
|
this.internalSelection = new HashSet<>();
|
||||||
|
this.selectedConversations = new MutableLiveData<>(new ConversationSet());
|
||||||
this.searchRepository = searchRepository;
|
this.searchRepository = searchRepository;
|
||||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||||
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
||||||
|
@ -72,7 +87,9 @@ class ConversationListViewModel extends ViewModel {
|
||||||
this.updateDebouncer = new ThrottledDebouncer(500);
|
this.updateDebouncer = new ThrottledDebouncer(500);
|
||||||
this.activeSearchResult = SearchResult.EMPTY;
|
this.activeSearchResult = SearchResult.EMPTY;
|
||||||
this.invalidator = new Invalidator();
|
this.invalidator = new Invalidator();
|
||||||
this.pagedData = PagedData.create(ConversationListDataSource.create(application, isArchived),
|
this.disposables = new CompositeDisposable();
|
||||||
|
this.conversationListDataSource = ConversationListDataSource.create(application, isArchived);
|
||||||
|
this.pagedData = PagedData.create(conversationListDataSource,
|
||||||
new PagingConfig.Builder()
|
new PagingConfig.Builder()
|
||||||
.setPageSize(15)
|
.setPageSize(15)
|
||||||
.setBufferPages(2)
|
.setBufferPages(2)
|
||||||
|
@ -142,6 +159,48 @@ class ConversationListViewModel extends ViewModel {
|
||||||
coldStart = false;
|
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) {
|
void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
|
||||||
megaphone.postValue(null);
|
megaphone.postValue(null);
|
||||||
megaphoneRepository.markFinished(event);
|
megaphoneRepository.markFinished(event);
|
||||||
|
@ -210,6 +269,7 @@ class ConversationListViewModel extends ViewModel {
|
||||||
@Override
|
@Override
|
||||||
protected void onCleared() {
|
protected void onCleared() {
|
||||||
invalidator.invalidate();
|
invalidator.invalidate();
|
||||||
|
disposables.dispose();
|
||||||
messageSearchDebouncer.clear();
|
messageSearchDebouncer.clear();
|
||||||
updateDebouncer.clear();
|
updateDebouncer.clear();
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
|
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() {
|
public @Nullable String getIndividualRecipientId() {
|
||||||
return individualRecipientId;
|
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 {
|
enum ReadStatus {
|
||||||
|
|
Loading…
Add table
Reference in a new issue