diff --git a/app/src/main/java/org/thoughtcrime/securesms/OverlayTransformation.kt b/app/src/main/java/org/thoughtcrime/securesms/OverlayTransformation.kt new file mode 100644 index 000000000..7d60ed23b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/OverlayTransformation.kt @@ -0,0 +1,29 @@ +package org.thoughtcrime.securesms + +import android.graphics.Bitmap +import android.graphics.Canvas +import androidx.annotation.ColorInt +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.security.MessageDigest + +/** + * BitmapTransformation which overlays the given bitmap with the given color. + */ +class OverlayTransformation( + @ColorInt private val color: Int +) : BitmapTransformation() { + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update("${OverlayTransformation::class.java.name}$color".toByteArray(CHARSET)) + } + + override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { + val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(outBitmap) + + canvas.drawBitmap(toTransform, 0f, 0f, null) + canvas.drawColor(color) + + return outBitmap + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 3f8cd5e83..cae208349 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -18,28 +18,41 @@ package org.thoughtcrime.securesms.conversationlist; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.Bitmap; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.text.Spannable; import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.style.StyleSpan; import android.util.AttributeSet; import android.view.View; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.Transformations; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.makeramen.roundedimageview.RoundedDrawable; + +import org.signal.core.util.DimensionUnit; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.BindableConversationListItem; +import org.thoughtcrime.securesms.OverlayTransformation; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.Unbindable; import org.thoughtcrime.securesms.badges.BadgeImageView; @@ -47,8 +60,8 @@ import org.thoughtcrime.securesms.components.AlertView; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.DeliveryStatusView; import org.thoughtcrime.securesms.components.FromTextView; -import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.TypingIndicatorView; +import org.thoughtcrime.securesms.components.emoji.EmojiStrings; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; @@ -56,6 +69,7 @@ import org.thoughtcrime.securesms.database.model.LiveUpdateMessage; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.UpdateDescription; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; @@ -73,6 +87,7 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.util.Collections; import java.util.Locale; import java.util.Set; +import java.util.concurrent.ExecutionException; import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync; @@ -110,7 +125,6 @@ public final class ConversationListItem extends ConstraintLayout private int unreadCount; private AvatarImageView contactPhotoImage; - private ThumbnailView thumbnailView; private final Debouncer subjectViewClearDebouncer = new Debouncer(150); @@ -134,11 +148,9 @@ public final class ConversationListItem extends ConstraintLayout this.deliveryStatusIndicator = findViewById(R.id.conversation_list_item_status); this.alertView = findViewById(R.id.conversation_list_item_alert); this.contactPhotoImage = findViewById(R.id.conversation_list_item_avatar); - this.thumbnailView = findViewById(R.id.conversation_list_item_thumbnail); this.archivedView = findViewById(R.id.conversation_list_item_archived); this.unreadIndicator = findViewById(R.id.conversation_list_item_unread_indicator); this.badge = findViewById(R.id.conversation_list_item_badge); - thumbnailView.setClickable(false); } @Override @@ -184,7 +196,7 @@ public final class ConversationListItem extends ConstraintLayout this.typingThreads = typingThreads; updateTypingIndicator(typingThreads); - observeDisplayBody(getThreadDisplayBody(getContext(), thread)); + observeDisplayBody(getThreadDisplayBody(getContext(), thread, glideRequests)); if (thread.getDate() > 0) { CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate()); @@ -201,7 +213,6 @@ public final class ConversationListItem extends ConstraintLayout } setStatusIcons(thread); - setThumbnailSnippet(thread); setBatchMode(batchMode); setRippleColor(recipient.get()); badge.setBadgeFromRecipient(recipient.get()); @@ -230,10 +241,10 @@ public final class ConversationListItem extends ConstraintLayout unreadIndicator.setVisibility(GONE); deliveryStatusIndicator.setNone(); alertView.setNone(); - thumbnailView.setVisibility(GONE); setBatchMode(false); setRippleColor(contact); + badge.setBadgeFromRecipient(recipient.get()); contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode); } @@ -258,10 +269,10 @@ public final class ConversationListItem extends ConstraintLayout unreadIndicator.setVisibility(GONE); deliveryStatusIndicator.setNone(); alertView.setNone(); - thumbnailView.setVisibility(GONE); setBatchMode(false); setRippleColor(recipient.get()); + badge.setBadgeFromRecipient(recipient.get()); contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode); } @@ -351,15 +362,6 @@ public final class ConversationListItem extends ConstraintLayout } } - private void setThumbnailSnippet(ThreadRecord thread) { - if (thread.getSnippetUri() != null) { - this.thumbnailView.setVisibility(VISIBLE); - this.thumbnailView.setImageResource(glideRequests, thread.getSnippetUri()); - } else { - this.thumbnailView.setVisibility(GONE); - } - } - private void setStatusIcons(ThreadRecord thread) { if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) { deliveryStatusIndicator.setNone(); @@ -435,7 +437,7 @@ public final class ConversationListItem extends ConstraintLayout badge.setBadgeFromRecipient(recipient); } - private static @NonNull LiveData getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) { + private static @NonNull LiveData getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread, @NonNull GlideRequests glideRequests) { int defaultTint = ContextCompat.getColor(context, R.color.signal_text_secondary); if (!thread.isMessageRequestAccepted()) { @@ -510,13 +512,14 @@ public final class ConversationListItem extends ConstraintLayout String body = removeNewlines(thread.getBody()); LiveData finalBody = recipientToStringAsync(thread.getRecipient().getId(), threadRecipient -> { + CharSequence bodyWithMediaIcon = createFinalBodyWithMediaIcon(context, body, thread, glideRequests); if (threadRecipient.isGroup()) { RecipientId groupMessageSender = thread.getGroupMessageSender(); if (!groupMessageSender.isUnknown()) { - return createGroupMessageUpdateString(context, body, Recipient.resolved(groupMessageSender), thread.isRead()); + return createGroupMessageUpdateString(context, bodyWithMediaIcon, Recipient.resolved(groupMessageSender), thread.isRead()); } } - return new SpannableString(body); + return new SpannableString(bodyWithMediaIcon); }); return whileLoadingShow(body, finalBody); @@ -524,21 +527,79 @@ public final class ConversationListItem extends ConstraintLayout } } + @WorkerThread + private static CharSequence createFinalBodyWithMediaIcon(@NonNull Context context, + @NonNull String body, + @NonNull ThreadRecord thread, + @NonNull GlideRequests glideRequests) + { + if (thread.getSnippetUri() != null) { + try { + int thumbSize = (int) DimensionUnit.SP.toPixels(20f); + Bitmap thumb = glideRequests.asBitmap() + .load(new DecryptableStreamUriLoader.DecryptableUri(thread.getSnippetUri())) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .override(thumbSize, thumbSize) + .downsample(DownsampleStrategy.CENTER_OUTSIDE) + .transform( + new OverlayTransformation(ContextCompat.getColor(context, R.color.transparent_black_08)), + new CenterCrop() + ) + .submit() + .get(); + + RoundedDrawable drawable = RoundedDrawable.fromBitmap(thumb); + drawable.setBounds(0, 0, thumbSize, thumbSize); + drawable.setCornerRadius(DimensionUnit.DP.toPixels(4)); + drawable.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + + CharSequence span = SpanUtil.buildCenteredImageSpan(drawable); + + final String withoutPrefix; + + if (body.startsWith(EmojiStrings.GIF)) { + withoutPrefix = body.replace(EmojiStrings.GIF, ""); + } else if (body.startsWith(EmojiStrings.VIDEO)) { + withoutPrefix = body.replace(EmojiStrings.VIDEO, ""); + } else if (body.startsWith(EmojiStrings.PHOTO)) { + withoutPrefix = body.replace(EmojiStrings.PHOTO, ""); + } else if (thread.getExtra() != null && thread.getExtra().getStickerEmoji() != null && body.startsWith(thread.getExtra().getStickerEmoji())) { + withoutPrefix = body.replace(thread.getExtra().getStickerEmoji(), ""); + } else { + withoutPrefix = null; + } + + if (withoutPrefix != null) { + return new SpannableStringBuilder() + .append(span) + .append(withoutPrefix); + } else { + return body; + } + + } catch (ExecutionException | InterruptedException e) { + return new SpannableString(body); + } + } else { + return new SpannableString(body); + } + } + private static SpannableString createGroupMessageUpdateString(@NonNull Context context, - @NonNull String body, + @NonNull CharSequence body, @NonNull Recipient recipient, boolean read) { String sender = (recipient.isSelf() ? context.getString(R.string.MessageRecord_you) : recipient.getShortDisplayName(context)) + ": "; - SpannableString spannable = new SpannableString(sender + body); - spannable.setSpan(SpanUtil.getBoldSpan(), - 0, - sender.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpannableStringBuilder builder = new SpannableStringBuilder(sender).append(body); + builder.setSpan(SpanUtil.getBoldSpan(), + 0, + sender.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - return spannable; + return new SpannableString(builder); } /** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CenteredImageSpan.java b/app/src/main/java/org/thoughtcrime/securesms/util/CenteredImageSpan.java new file mode 100644 index 000000000..64f32a661 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CenteredImageSpan.java @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.util; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.style.ReplacementSpan; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Centers the given drawable in the bounds of a single line of text, regardless of the size of the given drawable. + */ +public class CenteredImageSpan extends ReplacementSpan { + + private final Drawable drawable; + + public CenteredImageSpan(@NonNull Drawable drawable) { + this.drawable = drawable; + } + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) { + return drawable.getBounds().right; + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { + canvas.save(); + int transY = top + (bottom - top) / 2 - drawable.getBounds().height() / 2; + canvas.translate(x, transY); + drawable.draw(canvas); + canvas.restore(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java index 0e5310459..5473948f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import android.content.res.Resources; +import android.graphics.Bitmap; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; @@ -121,6 +122,12 @@ public final class SpanUtil { return imageSpan; } + public static CharSequence buildCenteredImageSpan(@NonNull Drawable drawable) { + SpannableString imageSpan = new SpannableString(" "); + imageSpan.setSpan(new CenteredImageSpan(drawable), 0, imageSpan.length(), 0); + return imageSpan; + } + public static CharSequence learnMore(@NonNull Context context, @ColorInt int color, @NonNull View.OnClickListener onLearnMoreClicked) diff --git a/app/src/main/res/drawable-night/unread_count_background_new.xml b/app/src/main/res/drawable-night/unread_count_background_new.xml index 01de9a90b..d25663fc7 100644 --- a/app/src/main/res/drawable-night/unread_count_background_new.xml +++ b/app/src/main/res/drawable-night/unread_count_background_new.xml @@ -7,8 +7,4 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable/unread_count_background_new.xml b/app/src/main/res/drawable/unread_count_background_new.xml index 4d8b1af10..116fdefd7 100644 --- a/app/src/main/res/drawable/unread_count_background_new.xml +++ b/app/src/main/res/drawable/unread_count_background_new.xml @@ -6,9 +6,4 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_list_item_view.xml b/app/src/main/res/layout/conversation_list_item_view.xml index 07401c80f..4049f37f0 100644 --- a/app/src/main/res/layout/conversation_list_item_view.xml +++ b/app/src/main/res/layout/conversation_list_item_view.xml @@ -23,24 +23,6 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/ic_contact_picture" /> - - - - + + - - + app:constraint_referenced_ids="conversation_list_item_status_container" /> - - + app:constraint_referenced_ids="conversation_list_item_date" /> 10dp 12.5dp - 25dp + 18dp 4dp 41dp