mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add long-press context menu in all media screen.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:enterFadeDuration="100"
|
||||
android:exitFadeDuration="100">
|
||||
|
||||
<item android:state_selected="true">
|
||||
<inset
|
||||
android:insetBottom="2dp"
|
||||
android:insetLeft="12dp"
|
||||
android:insetRight="12dp"
|
||||
android:insetTop="2dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/conversation_list_selected_color" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</inset>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<ripple android:color="@color/conversation_list_selected_color">
|
||||
<item android:id="@android:id/mask">
|
||||
<inset
|
||||
android:insetBottom="2dp"
|
||||
android:insetLeft="12dp"
|
||||
android:insetRight="12dp"
|
||||
android:insetTop="2dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/transparent_black_60" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</inset>
|
||||
</item>
|
||||
</ripple>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:enterFadeDuration="100"
|
||||
android:exitFadeDuration="100">
|
||||
<item android:state_selected="true">
|
||||
<color android:color="@color/transparent_black_20" />
|
||||
</item>
|
||||
<item>
|
||||
<ripple android:color="@color/transparent_black_30">
|
||||
<item android:id="@android:id/mask">
|
||||
<color android:color="@android:color/white" />
|
||||
</item>
|
||||
</ripple>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/media_overview_detail_item_background"
|
||||
android:minHeight="@dimen/media_overview_detail_item_height">
|
||||
|
||||
<FrameLayout
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/media_overview_detail_item_background"
|
||||
android:minHeight="@dimen/media_overview_detail_item_height">
|
||||
|
||||
<FrameLayout
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/media_overview_detail_item_background"
|
||||
android:minHeight="@dimen/media_overview_detail_item_height">
|
||||
|
||||
<FrameLayout
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/media_overview_detail_item_background"
|
||||
android:minHeight="@dimen/media_overview_detail_item_height">
|
||||
|
||||
<FrameLayout
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="@drawable/media_overview_item_selected_foreground">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
android:id="@+id/image"
|
||||
|
||||
@@ -1834,6 +1834,7 @@
|
||||
<string name="MediaOverviewActivity_sent_by_you_to_s">Sent by you to %1$s</string>
|
||||
<!-- Error message shown when the user is trying to open a media that is not sent yet. -->
|
||||
<string name="MediaOverviewActivity_this_media_is_not_sent_yet">This media is not sent yet.</string>
|
||||
<string name="MediaOverviewActivity_jump_to_message">Jump to message</string>
|
||||
|
||||
<!-- Megaphones -->
|
||||
<string name="Megaphones_remind_me_later">Remind me later</string>
|
||||
|
||||
Reference in New Issue
Block a user