mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 18:55:12 +00:00
Show a popup menu when long-pressing on the conversation list.
This commit is contained in:
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.content.res.use
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -13,7 +12,6 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
/**
|
||||
* A custom context menu that will show next to an anchor view and display several options. Basically a PopupMenu with custom UI and positioning rules.
|
||||
*
|
||||
* This will prefer showing the menu underneath the anchor, but if there's not enough space in the container, it will show it above the anchor and reverse the
|
||||
* order of the menu items. If there's not enough room for either, it'll show it centered above the anchor. If there's not enough room then, it'll center it,
|
||||
* chop off the part that doesn't fit, and make the menu scrollable.
|
||||
*/
|
||||
class SignalContextMenu private constructor(
|
||||
val anchor: View,
|
||||
val container: View,
|
||||
val items: List<Item>,
|
||||
val baseOffsetX: Int = 0,
|
||||
val baseOffsetY: Int = 0,
|
||||
val onDismiss: Runnable? = null
|
||||
) : PopupWindow(
|
||||
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
) {
|
||||
|
||||
val context: Context = anchor.context
|
||||
|
||||
val mappingAdapter = MappingAdapter().apply {
|
||||
registerFactory(DisplayItem::class.java, ItemViewHolderFactory())
|
||||
}
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
|
||||
isFocusable = true
|
||||
|
||||
if (onDismiss != null) {
|
||||
setOnDismissListener { onDismiss.run() }
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
elevation = 20f
|
||||
}
|
||||
|
||||
contentView.findViewById<RecyclerView>(R.id.signal_context_menu_list).apply {
|
||||
adapter = mappingAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
mappingAdapter.submitList(items.toAdapterItems())
|
||||
}
|
||||
|
||||
private fun show() {
|
||||
if (anchor.width == 0 || anchor.height == 0) {
|
||||
anchor.post(this::show)
|
||||
return
|
||||
}
|
||||
|
||||
contentView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
|
||||
val menuBottomBound = anchor.y + anchor.height + contentView.measuredHeight + baseOffsetY
|
||||
val menuTopBound = anchor.y - contentView.measuredHeight - baseOffsetY
|
||||
|
||||
val screenBottomBound = container.height
|
||||
val screenTopBound = container.y
|
||||
|
||||
val offsetY: Int
|
||||
|
||||
if (menuBottomBound < screenBottomBound) {
|
||||
offsetY = baseOffsetY
|
||||
} else if (menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchor.height + contentView.measuredHeight + baseOffsetY)
|
||||
mappingAdapter.submitList(items.reversed().toAdapterItems())
|
||||
} else {
|
||||
offsetY = -((anchor.height / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
|
||||
}
|
||||
|
||||
showAsDropDown(anchor, baseOffsetX, offsetY)
|
||||
}
|
||||
|
||||
private fun List<Item>.toAdapterItems(): List<DisplayItem> {
|
||||
return this.mapIndexed { index, item ->
|
||||
val displayType: DisplayType = when {
|
||||
this.size == 1 -> DisplayType.ONLY
|
||||
index == 0 -> DisplayType.TOP
|
||||
index == this.size - 1 -> DisplayType.BOTTOM
|
||||
else -> DisplayType.MIDDLE
|
||||
}
|
||||
|
||||
DisplayItem(item, displayType)
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
@DrawableRes val iconRes: Int,
|
||||
@StringRes val titleRes: Int,
|
||||
val action: Runnable
|
||||
)
|
||||
|
||||
private data class DisplayItem(
|
||||
val item: Item,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
}
|
||||
|
||||
enum class DisplayType {
|
||||
TOP, BOTTOM, MIDDLE, ONLY
|
||||
}
|
||||
|
||||
private inner class ItemViewHolder(itemView: View) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||
|
||||
override fun bind(model: DisplayItem) {
|
||||
icon.setImageResource(model.item.iconRes)
|
||||
title.setText(model.item.titleRes)
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ItemViewHolderFactory : MappingAdapter.Factory<DisplayItem> {
|
||||
override fun createViewHolder(parent: ViewGroup): MappingViewHolder<DisplayItem> {
|
||||
return ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.signal_context_menu_item, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param anchor The view to put the pop-up on
|
||||
* @param container A parent of [anchor] that represents the acceptable boundaries of the popup
|
||||
*/
|
||||
class Builder(
|
||||
val anchor: View,
|
||||
val container: View
|
||||
) {
|
||||
|
||||
var onDismiss: Runnable? = null
|
||||
var offsetX: Int = 0
|
||||
var offsetY: Int = 0
|
||||
|
||||
fun onDismiss(onDismiss: Runnable): Builder {
|
||||
this.onDismiss = onDismiss
|
||||
return this
|
||||
}
|
||||
|
||||
fun offsetX(offsetPx: Int): Builder {
|
||||
this.offsetX = offsetPx
|
||||
return this
|
||||
}
|
||||
|
||||
fun offsetY(offsetPx: Int): Builder {
|
||||
this.offsetY = offsetPx
|
||||
return this
|
||||
}
|
||||
|
||||
fun show(items: List<Item>) {
|
||||
SignalContextMenu(
|
||||
anchor = anchor,
|
||||
container = container,
|
||||
items = items,
|
||||
baseOffsetX = offsetX,
|
||||
baseOffsetY = offsetY,
|
||||
onDismiss = onDismiss
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||
int position = holder.getAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
return onConversationClickListener.onConversationLongClick(getItem(position));
|
||||
return onConversationClickListener.onConversationLongClick(getItem(position), v);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -302,8 +302,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||
}
|
||||
|
||||
interface OnConversationClickListener {
|
||||
void onConversationClick(Conversation conversation);
|
||||
boolean onConversationLongClick(Conversation conversation);
|
||||
void onConversationClick(@NonNull Conversation conversation);
|
||||
boolean onConversationLongClick(@NonNull Conversation conversation, @NonNull View view);
|
||||
void onShowArchiveClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
@@ -79,6 +80,7 @@ import org.thoughtcrime.securesms.NewConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.components.SignalContextMenu;
|
||||
import org.thoughtcrime.securesms.components.UnreadPaymentsView;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
|
||||
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
|
||||
@@ -145,6 +147,8 @@ import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -762,12 +766,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
});
|
||||
}
|
||||
|
||||
private void handleMarkSelectedAsRead() {
|
||||
Context context = requireContext();
|
||||
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
private void handleMarkAsRead(@NonNull Collection<Long> ids) {
|
||||
Context context = requireContext();
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(selectedConversations, false);
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(ids, false);
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
@@ -781,12 +784,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
});
|
||||
}
|
||||
|
||||
private void handleMarkSelectedAsUnread() {
|
||||
Context context = requireContext();
|
||||
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
private void handleMarkAsUnread(@NonNull Collection<Long> ids) {
|
||||
Context context = requireContext();
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
DatabaseFactory.getThreadDatabase(context).setForcedUnread(selectedConversations);
|
||||
DatabaseFactory.getThreadDatabase(context).setForcedUnread(ids);
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
return null;
|
||||
}, none -> {
|
||||
@@ -806,8 +808,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleArchiveAllSelected() {
|
||||
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
private void handleArchive(@NonNull Collection<Long> ids, boolean showProgress) {
|
||||
Set<Long> selectedConversations = new HashSet<>(ids);
|
||||
int count = selectedConversations.size();
|
||||
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
|
||||
|
||||
@@ -816,7 +818,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
snackBarTitle,
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG, true)
|
||||
Snackbar.LENGTH_LONG,
|
||||
showProgress)
|
||||
{
|
||||
|
||||
@Override
|
||||
@@ -838,22 +841,23 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
protected void reverseAction(@Nullable Void parameter) {
|
||||
reverseArchiveThreads(selectedConversations);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleDeleteAllSelected() {
|
||||
int conversationsCount = defaultAdapter.getBatchSelectionIds().size();
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
|
||||
alert.setIcon(R.drawable.ic_warning);
|
||||
alert.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_delete_selected_conversations,
|
||||
conversationsCount, conversationsCount));
|
||||
alert.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_this_will_permanently_delete_all_n_selected_conversations,
|
||||
conversationsCount, conversationsCount));
|
||||
private void handleDelete(@NonNull Collection<Long> ids) {
|
||||
int conversationsCount = ids.size();
|
||||
MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(requireActivity());
|
||||
Context context = requireContext();
|
||||
|
||||
alert.setTitle(context.getResources().getQuantityString(R.plurals.ConversationListFragment_delete_selected_conversations,
|
||||
conversationsCount, conversationsCount));
|
||||
alert.setMessage(context.getResources().getQuantityString(R.plurals.ConversationListFragment_this_will_permanently_delete_all_n_selected_conversations,
|
||||
conversationsCount, conversationsCount));
|
||||
alert.setCancelable(true);
|
||||
|
||||
alert.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
final Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
final Set<Long> selectedConversations = new HashSet<>(ids);
|
||||
|
||||
if (!selectedConversations.isEmpty()) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@@ -861,16 +865,16 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
dialog = ProgressDialog.show(getActivity(),
|
||||
getActivity().getString(R.string.ConversationListFragment_deleting),
|
||||
getActivity().getString(R.string.ConversationListFragment_deleting_selected_conversations),
|
||||
dialog = ProgressDialog.show(requireActivity(),
|
||||
context.getString(R.string.ConversationListFragment_deleting),
|
||||
context.getString(R.string.ConversationListFragment_deleting_selected_conversations),
|
||||
true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(getActivity());
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(requireActivity());
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -882,7 +886,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
actionMode = null;
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -890,8 +894,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
alert.show();
|
||||
}
|
||||
|
||||
private void handlePinAllSelected() {
|
||||
final Set<Long> toPin = new LinkedHashSet<>(Stream.of(defaultAdapter.getBatchSelection())
|
||||
private void handlePin(@NonNull Collection<Conversation> conversations) {
|
||||
final Set<Long> toPin = new LinkedHashSet<>(Stream.of(conversations)
|
||||
.filterNot(conversation -> conversation.getThreadRecord().isPinned())
|
||||
.map(conversation -> conversation.getThreadRecord().getThreadId())
|
||||
.toList());
|
||||
@@ -902,7 +906,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
Snackbar.LENGTH_LONG)
|
||||
.setTextColor(Color.WHITE)
|
||||
.show();
|
||||
actionMode.finish();
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -919,13 +925,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
});
|
||||
}
|
||||
|
||||
private void handleUnpinAllSelected() {
|
||||
final Set<Long> toPin = new HashSet<>(defaultAdapter.getBatchSelectionIds());
|
||||
|
||||
private void handleUnpin(@NonNull Collection<Long> ids) {
|
||||
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
||||
ThreadDatabase db = DatabaseFactory.getThreadDatabase(ApplicationDependencies.getApplication());
|
||||
|
||||
db.unpinConversations(toPin);
|
||||
db.unpinConversations(ids);
|
||||
|
||||
return null;
|
||||
}, unused -> {
|
||||
@@ -1005,7 +1009,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConversationClick(Conversation conversation) {
|
||||
public void onConversationClick(@NonNull Conversation conversation) {
|
||||
if (actionMode == null) {
|
||||
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
|
||||
} else {
|
||||
@@ -1021,16 +1025,51 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onConversationLongClick(Conversation conversation) {
|
||||
public boolean onConversationLongClick(@NonNull Conversation conversation, @NonNull View view) {
|
||||
if (actionMode != null) {
|
||||
onConversationClick(conversation);
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultAdapter.initializeBatchMode(true);
|
||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
||||
view.setSelected(true);
|
||||
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
|
||||
Collection<Long> id = Collections.singleton(conversation.getThreadRecord().getThreadId());
|
||||
|
||||
List<SignalContextMenu.Item> items = new ArrayList<>();
|
||||
|
||||
if (!conversation.getThreadRecord().isArchived()) {
|
||||
if (conversation.getThreadRecord().isRead()) {
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_unread_24, R.string.ConversationListFragment_unread, () -> handleMarkAsUnread(id)));
|
||||
} else {
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_read_24, R.string.ConversationListFragment_read, () -> handleMarkAsRead(id)));
|
||||
}
|
||||
|
||||
if (conversation.getThreadRecord().isPinned()) {
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_unpin_24, R.string.ConversationListFragment_unpin, () -> handleUnpin(id)));
|
||||
} else {
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_pin_24, R.string.ConversationListFragment_pin, () -> handlePin(Collections.singleton(conversation))));
|
||||
}
|
||||
}
|
||||
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_select_24, R.string.ConversationListFragment_select, () -> {
|
||||
defaultAdapter.initializeBatchMode(true);
|
||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
|
||||
}));
|
||||
|
||||
if (conversation.getThreadRecord().isArchived()) {
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_unarchive_24, R.string.ConversationListFragment_unarchive, () -> handleArchive(id, false)));
|
||||
} else {
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_archive_24, R.string.ConversationListFragment_archive, () -> handleArchive(id, false)));
|
||||
}
|
||||
|
||||
items.add(new SignalContextMenu.Item(R.drawable.ic_delete_24, R.string.ConversationListFragment_delete, () -> handleDelete(id)));
|
||||
|
||||
new SignalContextMenu.Builder(view, list)
|
||||
.offsetX(ViewUtil.dpToPx(12))
|
||||
.offsetY(ViewUtil.dpToPx(12))
|
||||
.onDismiss(() -> view.setSelected(false))
|
||||
.show(items);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1059,13 +1098,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_select_all: handleSelectAllThreads(); return true;
|
||||
case R.id.menu_delete_selected: handleDeleteAllSelected(); return true;
|
||||
case R.id.menu_pin_selected: handlePinAllSelected(); return true;
|
||||
case R.id.menu_unpin_selected: handleUnpinAllSelected(); return true;
|
||||
case R.id.menu_archive_selected: handleArchiveAllSelected(); return true;
|
||||
case R.id.menu_mark_as_read: handleMarkSelectedAsRead(); return true;
|
||||
case R.id.menu_mark_as_unread: handleMarkSelectedAsUnread(); return true;
|
||||
case R.id.menu_select_all: handleSelectAllThreads(); return true;
|
||||
case R.id.menu_delete_selected: handleDelete(defaultAdapter.getBatchSelectionIds()); return true;
|
||||
case R.id.menu_pin_selected: handlePin(defaultAdapter.getBatchSelection()); return true;
|
||||
case R.id.menu_unpin_selected: handleUnpin(defaultAdapter.getBatchSelectionIds()); return true;
|
||||
case R.id.menu_archive_selected: handleArchive(defaultAdapter.getBatchSelectionIds(), true); return true;
|
||||
case R.id.menu_mark_as_read: handleMarkAsRead(defaultAdapter.getBatchSelectionIds()); return true;
|
||||
case R.id.menu_mark_as_unread: handleMarkAsUnread(defaultAdapter.getBatchSelectionIds()); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -867,7 +867,7 @@ public class ThreadDatabase extends Database {
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
public void unpinConversations(@NonNull Set<Long> threadIds) {
|
||||
public void unpinConversations(@NonNull Collection<Long> threadIds) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
String placeholders = StringUtil.join(Stream.of(threadIds).map(unused -> "?").toList(), ",");
|
||||
|
||||
Reference in New Issue
Block a user