Add links to the all media view.

This commit is contained in:
Greyson Parrelli
2026-03-21 09:34:23 -04:00
committed by Cody Henthorne
parent 25b01a30be
commit 08491579dd
8 changed files with 323 additions and 20 deletions

View File

@@ -61,8 +61,9 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
${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.FROM_RECIPIENT_ID},
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID,
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS}
FROM
${AttachmentTable.TABLE_NAME}
LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
@@ -135,6 +136,69 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
"""
)
private val LINK_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},
${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.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.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.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.FROM_RECIPIENT_ID},
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID,
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS}
FROM
${MessageTable.TABLE_NAME}
LEFT JOIN ${AttachmentTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
AND ${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE} = 0
AND ${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_ID} IS NULL
LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}
WHERE
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} __EQUALITY__ ? AND
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS} IS NOT NULL AND
${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} = 0 AND
${MessageTable.TABLE_NAME}.${MessageTable.STORY_TYPE} = 0 AND
${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL AND
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} > 0 AND
$THREAD_RECIPIENT_ID > 0 AND
${MessageTable.TABLE_NAME}.${MessageTable.SCHEDULED_DATE} < 0
"""
private val DOCUMENT_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
"""
@@ -180,6 +244,17 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
return readableDatabase.rawQuery(query, args)
}
fun getLinkMediaForThread(threadId: Long, sorting: Sorting): Cursor {
val orderBy = when (sorting) {
Sorting.Newest -> " ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT} DESC"
Sorting.Oldest -> " ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT} ASC"
Sorting.Largest -> " ORDER BY ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE} DESC"
}
val query = applyEqualityOperator(threadId, LINK_MEDIA_QUERY) + orderBy
val args = arrayOf(threadId.toString())
return readableDatabase.rawQuery(query, args)
}
fun getAllMediaForThread(threadId: Long, sorting: Sorting): Cursor {
val query = sorting.applyToQuery(applyEqualityOperator(threadId, ALL_MEDIA_QUERY))
val args = arrayOf(threadId.toString() + "")
@@ -236,7 +311,8 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
val threadRecipientId: RecipientId,
val threadId: Long,
val date: Long,
val isOutgoing: Boolean
val isOutgoing: Boolean,
val linkPreviewJson: String? = null
) {
val contentType: String?
@@ -245,8 +321,11 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
companion object {
@JvmStatic
fun from(cursor: Cursor): MediaRecord {
val linkPreviewIdx = cursor.getColumnIndex(MessageTable.LINK_PREVIEWS)
val attachmentIdIdx = cursor.getColumnIndex(AttachmentTable.ID)
val hasAttachment = attachmentIdIdx != -1 && !cursor.isNull(attachmentIdIdx)
return MediaRecord(
attachment = SignalDatabase.attachments.getAttachment(cursor),
attachment = if (hasAttachment) SignalDatabase.attachments.getAttachment(cursor) else null,
recipientId = RecipientId.from(cursor.requireLong(MessageTable.FROM_RECIPIENT_ID)),
threadId = cursor.requireLong(MessageTable.THREAD_ID),
threadRecipientId = RecipientId.from(cursor.requireLong(THREAD_RECIPIENT_ID)),
@@ -255,7 +334,8 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
} else {
cursor.requireLong(MessageTable.DATE_RECEIVED)
},
isOutgoing = MessageTypes.isOutgoingMessageType(cursor.requireLong(MessageTable.TYPE))
isOutgoing = MessageTypes.isOutgoingMessageType(cursor.requireLong(MessageTable.TYPE)),
linkPreviewJson = if (linkPreviewIdx != -1) cursor.getString(linkPreviewIdx) else null
)
}
}

View File

@@ -14,6 +14,7 @@ public abstract class MediaLoader extends AbstractCursorLoader {
GALLERY,
DOCUMENT,
AUDIO,
LINK,
ALL
}
}

View File

@@ -40,6 +40,7 @@ public final class ThreadMediaLoader extends MediaLoader {
case GALLERY : return mediaDatabase.getGalleryMediaForThread(threadId, sorting);
case DOCUMENT: return mediaDatabase.getDocumentMediaForThread(threadId, sorting);
case AUDIO : return mediaDatabase.getAudioMediaForThread(threadId, sorting);
case LINK : return mediaDatabase.getLinkMediaForThread(threadId, sorting);
case ALL : return mediaDatabase.getAllMediaForThread(threadId, sorting);
default : throw new AssertionError();
}

View File

@@ -32,6 +32,10 @@ import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.RecyclerView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.bumptech.glide.RequestManager;
@@ -53,6 +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;
@@ -89,6 +94,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
public static final int GALLERY = 2;
private static final int GALLERY_DETAIL = 3;
private static final int DOCUMENT_DETAIL = 4;
private static final int LINK_DETAIL = 5;
private static final int PAYLOAD_SELECTED = 1;
@@ -142,6 +148,8 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
return new GalleryDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_media, parent, false));
case AUDIO_DETAIL:
return new AudioDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_audio, parent, false));
case LINK_DETAIL:
return new LinkDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_link, parent, false));
default:
return new DocumentDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_document, parent, false));
}
@@ -150,7 +158,11 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
@Override
public int getSectionItemViewType(int section, int offset) {
MediaTable.MediaRecord mediaRecord = media.get(section, offset);
Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment());
if (mediaRecord.getLinkPreviewJson() != null) return LINK_DETAIL;
if (mediaRecord.getAttachment() == null) return 0;
Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment());
if (slide.hasAudio()) return AUDIO_DETAIL;
if (slide.hasImage() || slide.hasVideo()) return detailView ? GALLERY_DETAIL : GALLERY;
@@ -177,7 +189,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
@Override
public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) {
MediaTable.MediaRecord mediaRecord = media.get(section, offset);
Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment());
Slide slide = mediaRecord.getAttachment() != null ? MediaUtil.getSlideForAttachment(mediaRecord.getAttachment()) : null;
((SelectableViewHolder) viewHolder).bind(context, mediaRecord, slide);
}
@@ -209,6 +221,8 @@ 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);
if (removed == null) {
@@ -244,7 +258,9 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
int sectionItemCount = media.getSectionItemCount(section);
for (int item = 0; item < sectionItemCount; item++) {
MediaRecord mediaRecord = media.get(section, item);
selected.put(mediaRecord.getAttachment().attachmentId, mediaRecord);
if (mediaRecord.getAttachment() != null) {
selected.put(mediaRecord.getAttachment().attachmentId, mediaRecord);
}
}
}
this.notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED);
@@ -270,7 +286,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
this.selectedIndicator = itemView.findViewById(R.id.selected_indicator);
}
public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) {
public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @Nullable Slide slide) {
if (bound) {
unbind();
}
@@ -288,7 +304,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
protected boolean isSelected() {
return selected.containsKey(mediaRecord.getAttachment().attachmentId);
return mediaRecord.getAttachment() != null && selected.containsKey(mediaRecord.getAttachment().attachmentId);
}
protected void updateSelectedView() {
@@ -413,10 +429,10 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
@Override
public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) {
public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @Nullable Slide slide) {
super.bind(context, mediaRecord, slide);
fileName = slide.getFileName();
fileName = slide != null ? slide.getFileName() : Optional.empty();
fileTypeDescription = getFileTypeDescription(context, slide);
line1.setText(fileName.orElse(fileTypeDescription));
@@ -449,14 +465,17 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
super.unbind();
}
private String getLine2(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) {
private String getLine2(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @Nullable Slide slide) {
if (slide == null) {
return DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), mediaRecord.getDate());
}
return context.getString(R.string.MediaOverviewActivity_detail_line_3_part,
new ByteSize(slide.getFileSize()).toUnitString(2),
getFileTypeDescription(context, slide),
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), mediaRecord.getDate()));
}
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
protected String getFileTypeDescription(@NonNull Context context, @Nullable Slide slide) {
return context.getString(R.string.MediaOverviewActivity_file);
}
@@ -609,7 +628,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
@Override
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
protected String getFileTypeDescription(@NonNull Context context, @Nullable Slide slide) {
return context.getString(R.string.MediaOverviewActivity_audio);
}
}
@@ -641,9 +660,9 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
@Override
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
if (slide.hasVideo()) return context.getString(R.string.MediaOverviewActivity_video);
if (slide.hasImage()) return context.getString(R.string.MediaOverviewActivity_image);
protected String getFileTypeDescription(@NonNull Context context, @Nullable Slide slide) {
if (slide != null && slide.hasVideo()) return context.getString(R.string.MediaOverviewActivity_video);
if (slide != null && slide.hasImage()) return context.getString(R.string.MediaOverviewActivity_image);
return super.getFileTypeDescription(context, slide);
}
@@ -660,6 +679,107 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
}
private class LinkDetailViewHolder extends DetailViewHolder {
private final ThumbnailView thumbnailView;
private final TextView linkUrlView;
private Slide slide;
private String linkUrl;
private String linkTitle;
LinkDetailViewHolder(@NonNull View itemView) {
super(itemView);
this.thumbnailView = itemView.findViewById(R.id.image);
this.linkUrlView = itemView.findViewById(R.id.link_url);
}
@Override
public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @Nullable Slide slide) {
parseLinkPreview(mediaRecord);
super.bind(context, mediaRecord, slide);
this.slide = slide;
if (slide != null) {
thumbnailView.setVisibility(View.VISIBLE);
thumbnailView.setImageResource(requestManager, slide, false, false);
} else {
thumbnailView.setVisibility(View.GONE);
}
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);
} else {
linkUrlView.setVisibility(View.GONE);
}
TextView line2View = itemView.findViewById(R.id.line2);
line2View.setText(DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), mediaRecord.getDate()));
}
@Override
protected @Nullable String getMediaTitle() {
if (linkTitle != null && !linkTitle.isEmpty()) {
return linkTitle;
}
return null;
}
@Override
protected @NonNull View getTransitionAnchor() {
return itemView;
}
@Override
protected String getFileTypeDescription(@NonNull Context context, @Nullable Slide slide) {
return context.getString(R.string.MediaOverviewActivity_link);
}
@Override
void rebind() {
if (slide != null) {
thumbnailView.setImageResource(requestManager, slide, false, false);
}
super.rebind();
}
@Override
void unbind() {
if (slide != null) {
thumbnailView.clear(requestManager);
}
super.unbind();
}
private void parseLinkPreview(@NonNull MediaTable.MediaRecord mediaRecord) {
linkUrl = "";
linkTitle = "";
if (mediaRecord.getLinkPreviewJson() != null) {
try {
JSONArray json = new JSONArray(mediaRecord.getLinkPreviewJson());
if (json.length() > 0) {
JSONObject preview = json.getJSONObject(0);
linkUrl = preview.optString("url", "");
linkTitle = preview.optString("title", "");
}
} catch (JSONException e) {
// ignore
}
}
}
}
private static final class AudioViewCallbacksAdapter implements AudioView.Callbacks {
private final AudioItemListener audioItemListener;

View File

@@ -128,7 +128,7 @@ public final class MediaOverviewActivity extends PassphraseRequiredActivity {
}
});
viewPager.setCurrentItem(allThreads ? 3 : 0);
viewPager.setCurrentItem(allThreads ? viewPager.getAdapter().getCount() - 1 : 0);
}
private static boolean allowGridSelectionOnPage(int page) {
@@ -264,10 +264,15 @@ public final class MediaOverviewActivity extends PassphraseRequiredActivity {
MediaOverviewPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
pages = new ArrayList<>(4);
boolean allThreads = threadId == MediaTable.ALL_THREADS;
pages = new ArrayList<>(allThreads ? 4 : 5);
pages.add(new Pair<>(MediaLoader.MediaType.GALLERY, getString(R.string.MediaOverviewActivity_Media)));
pages.add(new Pair<>(MediaLoader.MediaType.DOCUMENT, getString(R.string.MediaOverviewActivity_Files)));
pages.add(new Pair<>(MediaLoader.MediaType.AUDIO, getString(R.string.MediaOverviewActivity_Audio)));
if (!allThreads) {
pages.add(new Pair<>(MediaLoader.MediaType.LINK, getString(R.string.MediaOverviewActivity_Links)));
}
pages.add(new Pair<>(MediaLoader.MediaType.ALL, getString(R.string.MediaOverviewActivity_All)));
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?android:attr/colorControlHighlight" />
<corners android:radius="8dp" />
</shape>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/media_overview_detail_item_height">
<FrameLayout
android:id="@+id/image_container"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/dsl_settings_gutter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/link_fallback_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:background="@drawable/media_overview_link_fallback_bg"
android:importantForAccessibility="no"
android:padding="12dp"
android:scaleType="centerInside"
app:srcCompat="@drawable/symbol_link_24"
app:tint="@color/signal_icon_tint_secondary" />
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:contentDescription="@string/media_preview_activity__media_content_description"
app:thumbnail_radius="8dp"
app:transparent_overlay_color="@color/transparent_black_08" />
</FrameLayout>
<include layout="@layout/media_overview_selected_overlay" />
<LinearLayout
android:id="@+id/media_overview_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_container"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textAppearance="@style/Signal.Text.Body"
tools:text="Article Title - Sent by Alice" />
<TextView
android:id="@+id/link_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_text_secondary"
tools:text="https://example.com/some/long/article/path" />
<TextView
android:id="@+id/line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_text_secondary"
tools:text="11.06.19 at 5:25 AM" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1780,6 +1780,8 @@
<string name="MediaOverviewActivity_Files">Files</string>
<string name="MediaOverviewActivity_Audio">Audio</string>
<string name="MediaOverviewActivity_All">All</string>
<string name="MediaOverviewActivity_Links">Links</string>
<string name="MediaOverviewActivity_link">Link</string>
<plurals name="MediaOverviewActivity_Media_delete_confirm_title">
<item quantity="one">Delete selected item?</item>
<item quantity="other">Delete selected items?</item>