Add long-press context menu in all media screen.

This commit is contained in:
Greyson Parrelli
2026-04-07 12:15:50 -04:00
parent 4c76cb682e
commit 38eed43046
11 changed files with 218 additions and 9 deletions

View File

@@ -305,22 +305,26 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
protected void updateSelectedView() {
boolean selected = isSelected();
itemView.setSelected(selected);
if (selectedIndicator != null) {
selectedIndicator.animate().cancel();
selectedIndicator.setAlpha(isSelected() ? 1f : 0f);
selectedIndicator.setAlpha(selected ? 1f : 0f);
}
}
protected void animateSelectedView() {
boolean selected = isSelected();
itemView.setSelected(selected);
if (selectedIndicator != null) {
selectedIndicator.animate()
.alpha(isSelected() ? 1f : 0f)
.alpha(selected ? 1f : 0f)
.setDuration(SELECTION_ANIMATION_DURATION);
}
}
boolean onLongClick() {
itemClickListener.onMediaLongClicked(mediaRecord);
itemClickListener.onMediaLongClicked(itemView, mediaRecord);
return true;
}
@@ -817,7 +821,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
interface ItemClickListener {
void onMediaClicked(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord);
void onMediaLongClicked(MediaTable.MediaRecord mediaRecord);
void onMediaLongClicked(@NonNull View view, MediaTable.MediaRecord mediaRecord);
}
interface AudioItemListener {

View File

@@ -0,0 +1,108 @@
package org.thoughtcrime.securesms.mediaoverview
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.dp
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.signal.core.ui.R as CoreUiR
/**
* Context menu shown when long-pressing a media item in [MediaOverviewPageFragment].
*/
class MediaOverviewContextMenu(
private val fragment: Fragment,
private val callbacks: Callbacks
) {
private val lifecycleDisposable by lazy { LifecycleDisposable().bindTo(fragment.viewLifecycleOwner) }
fun show(anchor: View, mediaRecord: MediaTable.MediaRecord) {
val recyclerView = anchor.parent as? RecyclerView
recyclerView?.suppressLayout(true)
anchor.isSelected = true
SignalContextMenu.Builder(anchor, anchor.parent as ViewGroup)
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
.offsetY(4.dp)
.onDismiss {
anchor.isSelected = false
recyclerView?.suppressLayout(false)
}
.show(
listOfNotNull(
getSaveActionItem(mediaRecord),
getDeleteActionItem(mediaRecord),
getSelectActionItem(mediaRecord),
getJumpToMessageActionItem(mediaRecord)
)
)
}
private fun getSaveActionItem(mediaRecord: MediaTable.MediaRecord): ActionItem? {
if (mediaRecord.attachment == null) return null
return ActionItem(
iconRes = R.drawable.symbol_save_android_24,
title = fragment.getString(R.string.save)
) {
callbacks.onSave(mediaRecord)
}
}
private fun getDeleteActionItem(mediaRecord: MediaTable.MediaRecord): ActionItem {
return ActionItem(
iconRes = CoreUiR.drawable.symbol_trash_24,
title = fragment.getString(R.string.delete)
) {
callbacks.onDelete(mediaRecord)
}
}
private fun getSelectActionItem(mediaRecord: MediaTable.MediaRecord): ActionItem {
return ActionItem(
iconRes = CoreUiR.drawable.symbol_check_circle_24,
title = fragment.getString(R.string.CallContextMenu__select)
) {
callbacks.onSelect(mediaRecord)
}
}
private fun getJumpToMessageActionItem(mediaRecord: MediaTable.MediaRecord): ActionItem {
return ActionItem(
iconRes = R.drawable.symbol_open_24,
title = fragment.getString(R.string.MediaOverviewActivity_jump_to_message)
) {
lifecycleDisposable += Single.fromCallable<Int> {
val dateReceived = SignalDatabase.messages.getMessageRecordOrNull(mediaRecord.messageId)?.dateReceived
?: mediaRecord.date
SignalDatabase.messages.getMessagePositionInConversation(mediaRecord.threadId, dateReceived)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy { position ->
fragment.startActivity(
ConversationIntents.createBuilderSync(fragment.requireContext(), mediaRecord.threadRecipientId, mediaRecord.threadId)
.withStartingPosition(maxOf(0, position))
.build()
)
}
}
}
interface Callbacks {
fun onSave(mediaRecord: MediaTable.MediaRecord)
fun onDelete(mediaRecord: MediaTable.MediaRecord)
fun onSelect(mediaRecord: MediaTable.MediaRecord)
}
}

View File

@@ -63,6 +63,7 @@ import org.json.JSONException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -392,12 +393,52 @@ public final class MediaOverviewPageFragment extends LoggingFragment
}
@Override
public void onMediaLongClicked(MediaTable.MediaRecord mediaRecord) {
if (actionMode == null) {
enterMultiSelect();
public void onMediaLongClicked(@NonNull View view, MediaTable.MediaRecord mediaRecord) {
if (actionMode != null) {
handleMediaMultiSelectClick(mediaRecord);
return;
}
handleMediaMultiSelectClick(mediaRecord);
new MediaOverviewContextMenu(this, new MediaOverviewContextMenu.Callbacks() {
@Override
public void onSave(@NonNull MediaTable.MediaRecord record) {
handleSaveSingleMedia(record);
}
@Override
public void onDelete(@NonNull MediaTable.MediaRecord record) {
handleDeleteSingleMedia(record);
}
@Override
public void onSelect(@NonNull MediaTable.MediaRecord record) {
enterMultiSelect();
handleMediaMultiSelectClick(record);
}
}).show(view, mediaRecord);
}
private void handleSaveSingleMedia(@NonNull MediaTable.MediaRecord mediaRecord) {
if (SignalStore.backup().getOptimizeStorage() && mediaRecord.getAttachment() != null && !mediaRecord.getAttachment().hasData) {
OffloadedMediaDialogUtil.showAllOffloaded(requireContext());
return;
}
lifecycleDisposable.add(
MediaActions.handleSaveMedia(this, Collections.singleton(mediaRecord))
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
);
}
private void handleDeleteSingleMedia(@NonNull MediaTable.MediaRecord mediaRecord) {
if (DeleteSyncEducationDialog.shouldShow()) {
lifecycleDisposable.add(
DeleteSyncEducationDialog.show(getChildFragmentManager())
.subscribe(() -> handleDeleteSingleMedia(mediaRecord))
);
return;
}
MediaActions.handleDeleteMedia(requireContext(), Collections.singleton(mediaRecord));
}
private void handleDeleteSelectedMedia() {