Fix link multi-selection in media overview.

This commit is contained in:
Greyson Parrelli
2026-03-26 15:19:07 -04:00
parent 17faf56388
commit 2959e05ea7
6 changed files with 121 additions and 72 deletions

View File

@@ -20,65 +20,67 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
private val TAG = Log.tag(MediaTable::class)
const val ALL_THREADS = -1
private const val THREAD_RECIPIENT_ID = "THREAD_RECIPIENT_ID"
private const val MEDIA_MESSAGE_ID = "media_message_id"
private val BASE_MEDIA_QUERY = """
SELECT
${AttachmentTable.TABLE_NAME}.${AttachmentTable.ID} AS ${AttachmentTable.ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.CONTENT_TYPE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.TRANSFER_STATE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.FILE_NAME},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_FILE},
SELECT
${AttachmentTable.TABLE_NAME}.${AttachmentTable.ID} AS ${AttachmentTable.ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.CONTENT_TYPE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.TRANSFER_STATE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.FILE_NAME},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_FILE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.CDN_NUMBER},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_LOCATION},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_KEY},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_DIGEST},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_DIGEST},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.FAST_PREFLIGHT_ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.VOICE_NOTE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.BORDERLESS},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.VIDEO_GIF},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.WIDTH},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.HEIGHT},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.VOICE_NOTE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.BORDERLESS},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.VIDEO_GIF},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.WIDTH},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.HEIGHT},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE_TARGET_CONTENT_TYPE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_KEY},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_EMOJI},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.BLUR_HASH},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.TRANSFORM_PROPERTIES},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DISPLAY_ORDER},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.CAPTION},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_INCREMENTAL_DIGEST},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_KEY},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_ID},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_EMOJI},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.BLUR_HASH},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.TRANSFORM_PROPERTIES},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DISPLAY_ORDER},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.CAPTION},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_INCREMENTAL_DIGEST},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_INCREMENTAL_DIGEST_CHUNK_SIZE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH_END},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_CDN},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_RESTORE_STATE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_TRANSFER_STATE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.ATTACHMENT_UUID},
${MessageTable.TABLE_NAME}.${MessageTable.TYPE},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_SERVER},
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID},
${MessageTable.TABLE_NAME}.${MessageTable.TYPE},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_SERVER},
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID},
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID},
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID,
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS}
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} as $MEDIA_MESSAGE_ID
FROM
${AttachmentTable.TABLE_NAME} __INDEX_HINT__
LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}
WHERE
__THREAD_FILTER__ AND
(%s) AND
${MessageTable.VIEW_ONCE} = 0 AND
(%s) AND
${MessageTable.VIEW_ONCE} = 0 AND
${MessageTable.STORY_TYPE} = 0 AND
${MessageTable.LATEST_REVISION_ID} IS NULL AND
${AttachmentTable.QUOTE} = 0 AND
${AttachmentTable.STICKER_PACK_ID} IS NULL AND
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} > 0 AND
${MessageTable.LATEST_REVISION_ID} IS NULL AND
${AttachmentTable.QUOTE} = 0 AND
${AttachmentTable.STICKER_PACK_ID} IS NULL AND
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} > 0 AND
$THREAD_RECIPIENT_ID > 0
"""
@@ -179,7 +181,8 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID},
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID},
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID,
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS}
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS},
${MessageTable.TABLE_NAME}.${MessageTable.ID} as $MEDIA_MESSAGE_ID
FROM
${MessageTable.TABLE_NAME}
LEFT JOIN ${AttachmentTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
@@ -344,6 +347,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
val recipientId: RecipientId,
val threadRecipientId: RecipientId,
val threadId: Long,
val messageId: Long,
val date: Long,
val isOutgoing: Boolean,
val linkPreviewJson: String? = null
@@ -363,6 +367,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
recipientId = RecipientId.from(cursor.requireLong(MessageTable.FROM_RECIPIENT_ID)),
threadId = cursor.requireLong(MessageTable.THREAD_ID),
threadRecipientId = RecipientId.from(cursor.requireLong(THREAD_RECIPIENT_ID)),
messageId = cursor.requireLong(MEDIA_MESSAGE_ID),
date = if (MessageTypes.isPushType(cursor.requireLong(MessageTable.TYPE))) {
cursor.requireLong(MessageTable.DATE_SENT)
} else {

View File

@@ -185,6 +185,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThrea
@Override
public int groupForRecord(@NonNull MediaTable.MediaRecord mediaRecord) {
if (mediaRecord.getAttachment() == null) return SMALL;
long size = mediaRecord.getAttachment().size;
if (size < MB) return SMALL;

View File

@@ -11,6 +11,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.AttachmentSaver;
import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob;
import org.thoughtcrime.securesms.util.AttachmentUtil;
@@ -59,9 +60,17 @@ final class MediaActions {
Set<MessageRecord> deletedMessageRecords = new HashSet<>(records.length);
for (MediaTable.MediaRecord record : records) {
MessageRecord deleted = AttachmentUtil.deleteAttachment(record.getAttachment());
if (deleted != null) {
deletedMessageRecords.add(deleted);
if (record.getAttachment() != null) {
MessageRecord deleted = AttachmentUtil.deleteAttachment(record.getAttachment());
if (deleted != null) {
deletedMessageRecords.add(deleted);
}
} else {
MessageRecord deleted = SignalDatabase.messages().getMessageRecordOrNull(record.getMessageId());
SignalDatabase.messages().deleteMessage(record.getMessageId());
if (deleted != null) {
deletedMessageRecords.add(deleted);
}
}
}

View File

@@ -43,7 +43,7 @@ import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
import org.signal.core.util.ByteSize;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.ThumbnailView;
@@ -57,7 +57,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.signal.core.util.Util;
@@ -79,12 +79,12 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
private static final long SELECTION_ANIMATION_DURATION = TimeUnit.MILLISECONDS.toMillis(150);
private final Context context;
private final boolean showThread;
private final RequestManager requestManager;
private final ItemClickListener itemClickListener;
private final Map<AttachmentId, MediaRecord> selected = new HashMap<>();
private final AudioItemListener audioItemListener;
private final Context context;
private final boolean showThread;
private final RequestManager requestManager;
private final ItemClickListener itemClickListener;
private final Map<MediaSelectionKey, MediaRecord> selected = new HashMap<>();
private final AudioItemListener audioItemListener;
private GroupedThreadMedia media;
private boolean showFileSizes;
@@ -221,12 +221,10 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
public void toggleSelection(@NonNull MediaRecord mediaRecord) {
if (mediaRecord.getAttachment() == null) return;
AttachmentId attachmentId = mediaRecord.getAttachment().attachmentId;
MediaTable.MediaRecord removed = selected.remove(attachmentId);
MediaSelectionKey key = MediaSelectionKey.from(mediaRecord);
MediaTable.MediaRecord removed = selected.remove(key);
if (removed == null) {
selected.put(attachmentId, mediaRecord);
selected.put(key, mediaRecord);
}
notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED);
@@ -237,9 +235,8 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
public long getSelectedMediaTotalFileSize() {
//noinspection ConstantConditions attacment cannot be null if selected
return Stream.of(selected.values())
.collect(Collectors.summingLong(a -> a.getAttachment().size));
.collect(Collectors.summingLong(a -> a.getAttachment() != null ? a.getAttachment().size : 0));
}
@NonNull
@@ -258,9 +255,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
int sectionItemCount = media.getSectionItemCount(section);
for (int item = 0; item < sectionItemCount; item++) {
MediaRecord mediaRecord = media.get(section, item);
if (mediaRecord.getAttachment() != null) {
selected.put(mediaRecord.getAttachment().attachmentId, mediaRecord);
}
selected.put(MediaSelectionKey.from(mediaRecord), mediaRecord);
}
}
this.notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED);
@@ -274,6 +269,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
this.detailView = detailView;
}
class SelectableViewHolder extends ItemViewHolder {
protected final View selectedIndicator;
@@ -304,7 +300,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
protected boolean isSelected() {
return mediaRecord.getAttachment() != null && selected.containsKey(mediaRecord.getAttachment().attachmentId);
return selected.containsKey(MediaSelectionKey.from(mediaRecord));
}
protected void updateSelectedView() {
@@ -707,16 +703,9 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
thumbnailView.setVisibility(View.GONE);
}
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(getTransitionAnchor(), mediaRecord));
thumbnailView.setOnLongClickListener(view -> onLongClick());
View.OnClickListener openLink = view -> {
if (linkUrl != null && !linkUrl.isEmpty()) {
CommunicationActions.openBrowserLink(context, linkUrl);
}
};
thumbnailView.setOnClickListener(openLink);
itemView.setOnClickListener(openLink);
if (linkUrl != null && !linkUrl.isEmpty()) {
linkUrlView.setText(linkUrl);
linkUrlView.setVisibility(View.VISIBLE);

View File

@@ -52,10 +52,14 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.BottomOffsetDecoration;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.OffloadedMediaDialogUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
@@ -292,12 +296,20 @@ public final class MediaOverviewPageFragment extends LoggingFragment
}
private void handleMediaPreviewClick(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord) {
if (mediaRecord.getAttachment().getDisplayUri() == null) {
Context context = getContext();
if (context == null) {
return;
}
Context context = getContext();
if (context == null) {
if (mediaRecord.getLinkPreviewJson() != null) {
String url = parseLinkUrl(mediaRecord.getLinkPreviewJson());
if (url != null && !url.isEmpty()) {
CommunicationActions.openBrowserLink(context, url);
}
return;
}
if (mediaRecord.getAttachment() == null || mediaRecord.getAttachment().getDisplayUri() == null) {
return;
}
@@ -354,6 +366,18 @@ public final class MediaOverviewPageFragment extends LoggingFragment
}
}
private static @Nullable String parseLinkUrl(@NonNull String linkPreviewJson) {
try {
JSONArray json = new JSONArray(linkPreviewJson);
if (json.length() > 0) {
return json.getJSONObject(0).optString("url", "");
}
} catch (JSONException e) {
// ignore
}
return null;
}
@Override
public void onMediaLongClicked(MediaTable.MediaRecord mediaRecord) {
if (actionMode == null) {

View File

@@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.mediaoverview
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.database.MediaTable.MediaRecord
sealed class MediaSelectionKey {
data class Attachment(val attachmentId: AttachmentId) : MediaSelectionKey()
data class Message(val messageId: Long) : MediaSelectionKey()
companion object {
@JvmStatic
fun from(mediaRecord: MediaRecord): MediaSelectionKey {
val attachment = mediaRecord.attachment
return if (attachment != null) {
Attachment(attachment.attachmentId)
} else {
Message(mediaRecord.messageId)
}
}
}
}