diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt index c011b6e937..3750f02674 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt @@ -95,8 +95,10 @@ class BadgeImageView @JvmOverloads constructor( } private fun clearDrawable() { - setImageDrawable(null) - isClickable = false + if (drawable != null) { + setImageDrawable(null) + isClickable = false + } } private fun getGlideRequests(): GlideRequests? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt index 22ad39d217..9cea8285ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt @@ -59,73 +59,93 @@ class BadgeSpriteTransformation( return outBitmap } - enum class Size(val code: String, val frameMap: Map) { + enum class Size(val code: String) { SMALL( - "small", - mapOf( - Density.LDPI to FrameSet(Frame(124, 1, 12, 12), Frame(145, 31, 12, 12)), - Density.MDPI to FrameSet(Frame(163, 1, 16, 16), Frame(189, 39, 16, 16)), - Density.HDPI to FrameSet(Frame(244, 1, 24, 24), Frame(283, 58, 24, 24)), - Density.XHDPI to FrameSet(Frame(323, 1, 32, 32), Frame(373, 75, 32, 32)), - Density.XXHDPI to FrameSet(Frame(483, 1, 48, 48), Frame(557, 111, 48, 48)), - Density.XXXHDPI to FrameSet(Frame(643, 1, 64, 64), Frame(741, 147, 64, 64)) - ) - ), + "small" + ) { + override val frameMap: Map by lazy { + mapOf( + Density.LDPI to FrameSet(Frame(124, 1, 12, 12), Frame(145, 31, 12, 12)), + Density.MDPI to FrameSet(Frame(163, 1, 16, 16), Frame(189, 39, 16, 16)), + Density.HDPI to FrameSet(Frame(244, 1, 24, 24), Frame(283, 58, 24, 24)), + Density.XHDPI to FrameSet(Frame(323, 1, 32, 32), Frame(373, 75, 32, 32)), + Density.XXHDPI to FrameSet(Frame(483, 1, 48, 48), Frame(557, 111, 48, 48)), + Density.XXXHDPI to FrameSet(Frame(643, 1, 64, 64), Frame(741, 147, 64, 64)) + ) + } + }, MEDIUM( - "medium", - mapOf( - Density.LDPI to FrameSet(Frame(124, 16, 18, 18), Frame(160, 31, 18, 18)), - Density.MDPI to FrameSet(Frame(163, 19, 24, 24), Frame(207, 39, 24, 24)), - Density.HDPI to FrameSet(Frame(244, 28, 36, 36), Frame(310, 58, 36, 36)), - Density.XHDPI to FrameSet(Frame(323, 35, 48, 48), Frame(407, 75, 48, 48)), - Density.XXHDPI to FrameSet(Frame(483, 51, 72, 72), Frame(607, 111, 72, 72)), - Density.XXXHDPI to FrameSet(Frame(643, 67, 96, 96), Frame(807, 147, 96, 96)) - ) - ), + "medium" + ) { + override val frameMap: Map by lazy { + mapOf( + Density.LDPI to FrameSet(Frame(124, 16, 18, 18), Frame(160, 31, 18, 18)), + Density.MDPI to FrameSet(Frame(163, 19, 24, 24), Frame(207, 39, 24, 24)), + Density.HDPI to FrameSet(Frame(244, 28, 36, 36), Frame(310, 58, 36, 36)), + Density.XHDPI to FrameSet(Frame(323, 35, 48, 48), Frame(407, 75, 48, 48)), + Density.XXHDPI to FrameSet(Frame(483, 51, 72, 72), Frame(607, 111, 72, 72)), + Density.XXXHDPI to FrameSet(Frame(643, 67, 96, 96), Frame(807, 147, 96, 96)) + ) + } + }, LARGE( - "large", - mapOf( - Density.LDPI to FrameSet(Frame(145, 1, 27, 27), Frame(124, 46, 27, 27)), - Density.MDPI to FrameSet(Frame(189, 1, 36, 36), Frame(163, 57, 36, 36)), - Density.HDPI to FrameSet(Frame(283, 1, 54, 54), Frame(244, 85, 54, 54)), - Density.XHDPI to FrameSet(Frame(373, 1, 72, 72), Frame(323, 109, 72, 72)), - Density.XXHDPI to FrameSet(Frame(557, 1, 108, 108), Frame(483, 161, 108, 108)), - Density.XXXHDPI to FrameSet(Frame(741, 1, 144, 144), Frame(643, 213, 144, 144)) - ) - ), + "large" + ) { + override val frameMap: Map by lazy { + mapOf( + Density.LDPI to FrameSet(Frame(145, 1, 27, 27), Frame(124, 46, 27, 27)), + Density.MDPI to FrameSet(Frame(189, 1, 36, 36), Frame(163, 57, 36, 36)), + Density.HDPI to FrameSet(Frame(283, 1, 54, 54), Frame(244, 85, 54, 54)), + Density.XHDPI to FrameSet(Frame(373, 1, 72, 72), Frame(323, 109, 72, 72)), + Density.XXHDPI to FrameSet(Frame(557, 1, 108, 108), Frame(483, 161, 108, 108)), + Density.XXXHDPI to FrameSet(Frame(741, 1, 144, 144), Frame(643, 213, 144, 144)) + ) + } + }, BADGE_64( - "badge_64", - mapOf( - Density.LDPI to FrameSet(Frame(124, 73, 48, 48), Frame(124, 73, 48, 48)), - Density.MDPI to FrameSet(Frame(163, 97, 64, 64), Frame(163, 97, 64, 64)), - Density.HDPI to FrameSet(Frame(244, 145, 96, 96), Frame(244, 145, 96, 96)), - Density.XHDPI to FrameSet(Frame(323, 193, 128, 128), Frame(323, 193, 128, 128)), - Density.XXHDPI to FrameSet(Frame(483, 289, 192, 192), Frame(483, 289, 192, 192)), - Density.XXXHDPI to FrameSet(Frame(643, 385, 256, 256), Frame(643, 385, 256, 256)) - ) - ), + "badge_64" + ) { + override val frameMap: Map by lazy { + mapOf( + Density.LDPI to FrameSet(Frame(124, 73, 48, 48), Frame(124, 73, 48, 48)), + Density.MDPI to FrameSet(Frame(163, 97, 64, 64), Frame(163, 97, 64, 64)), + Density.HDPI to FrameSet(Frame(244, 145, 96, 96), Frame(244, 145, 96, 96)), + Density.XHDPI to FrameSet(Frame(323, 193, 128, 128), Frame(323, 193, 128, 128)), + Density.XXHDPI to FrameSet(Frame(483, 289, 192, 192), Frame(483, 289, 192, 192)), + Density.XXXHDPI to FrameSet(Frame(643, 385, 256, 256), Frame(643, 385, 256, 256)) + ) + } + }, BADGE_112( - "badge_112", - mapOf( - Density.LDPI to FrameSet(Frame(181, 1, 84, 84), Frame(181, 1, 84, 84)), - Density.MDPI to FrameSet(Frame(233, 1, 112, 112), Frame(233, 1, 112, 112)), - Density.HDPI to FrameSet(Frame(349, 1, 168, 168), Frame(349, 1, 168, 168)), - Density.XHDPI to FrameSet(Frame(457, 1, 224, 224), Frame(457, 1, 224, 224)), - Density.XXHDPI to FrameSet(Frame(681, 1, 336, 336), Frame(681, 1, 336, 336)), - Density.XXXHDPI to FrameSet(Frame(905, 1, 448, 448), Frame(905, 1, 448, 448)) - ) - ), + "badge_112" + ) { + override val frameMap: Map by lazy { + mapOf( + Density.LDPI to FrameSet(Frame(181, 1, 84, 84), Frame(181, 1, 84, 84)), + Density.MDPI to FrameSet(Frame(233, 1, 112, 112), Frame(233, 1, 112, 112)), + Density.HDPI to FrameSet(Frame(349, 1, 168, 168), Frame(349, 1, 168, 168)), + Density.XHDPI to FrameSet(Frame(457, 1, 224, 224), Frame(457, 1, 224, 224)), + Density.XXHDPI to FrameSet(Frame(681, 1, 336, 336), Frame(681, 1, 336, 336)), + Density.XXXHDPI to FrameSet(Frame(905, 1, 448, 448), Frame(905, 1, 448, 448)) + ) + } + }, XLARGE( - "xlarge", - mapOf( - Density.LDPI to FrameSet(Frame(1, 1, 120, 120), Frame(1, 1, 120, 120)), - Density.MDPI to FrameSet(Frame(1, 1, 160, 160), Frame(1, 1, 160, 160)), - Density.HDPI to FrameSet(Frame(1, 1, 240, 240), Frame(1, 1, 240, 240)), - Density.XHDPI to FrameSet(Frame(1, 1, 320, 320), Frame(1, 1, 320, 320)), - Density.XXHDPI to FrameSet(Frame(1, 1, 480, 480), Frame(1, 1, 480, 480)), - Density.XXXHDPI to FrameSet(Frame(1, 1, 640, 640), Frame(1, 1, 640, 640)) - ) - ); + "xlarge" + ) { + override val frameMap: Map by lazy { + mapOf( + Density.LDPI to FrameSet(Frame(1, 1, 120, 120), Frame(1, 1, 120, 120)), + Density.MDPI to FrameSet(Frame(1, 1, 160, 160), Frame(1, 1, 160, 160)), + Density.HDPI to FrameSet(Frame(1, 1, 240, 240), Frame(1, 1, 240, 240)), + Density.XHDPI to FrameSet(Frame(1, 1, 320, 320), Frame(1, 1, 320, 320)), + Density.XXHDPI to FrameSet(Frame(1, 1, 480, 480), Frame(1, 1, 480, 480)), + Density.XXXHDPI to FrameSet(Frame(1, 1, 640, 640), Frame(1, 1, 640, 640)) + ) + } + }; + + abstract val frameMap: Map companion object { fun fromInteger(integer: Int): Size { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index f4a529eb81..d05644e689 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -2303,6 +2303,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo return bodyBubble; } + @Override + public void invalidateChatColorsDrawable(@NonNull ViewGroup coordinateRoot) { + // Intentionally left blank. + } + private class SharedContactEventListener implements SharedContactView.EventListener { @Override public void onAddToContactsClicked(@NonNull Contact contact) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 9faa2ab984..2cd7347edc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -185,6 +185,7 @@ import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResults import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2 import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel +import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFragment import org.thoughtcrime.securesms.database.DraftTable @@ -579,6 +580,8 @@ class ConversationFragment : registerForResults() inputPanel.setMediaListener(InputPanelMediaListener()) + + ChatColorsDrawable.attach(binding.conversationItemRecycler) } override fun onViewStateRestored(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/ChatColorsDrawable.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/ChatColorsDrawable.kt new file mode 100644 index 0000000000..dbdc24bda9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/ChatColorsDrawable.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.v2.items + +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import androidx.core.graphics.withClip +import androidx.core.graphics.withTranslation +import androidx.core.view.children +import androidx.core.view.doOnDetach +import androidx.recyclerview.widget.RecyclerView +import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.util.Projection +import org.thoughtcrime.securesms.util.Projection.Corners + +/** + * Drawable that renders the given chat colors at a specified coordinate offset. + * This is meant to be used in conjunction with [ChatColorsItemDecoration] + */ +class ChatColorsDrawable : Drawable() { + + companion object { + private var maskDrawable: Drawable? = null + + /** + * Binds the ChatColorsDrawable static cache to the lifecycle of the given recycler-view + */ + fun attach(recyclerView: RecyclerView) { + recyclerView.addOnLayoutChangeListener { _, left, top, right, bottom, _, _, _, _ -> + applyBounds(Rect(left, top, right, bottom)) + } + + recyclerView.addItemDecoration(ChatColorsItemDecoration) + recyclerView.doOnDetach { + maskDrawable = null + } + } + + private fun applyBounds(bounds: Rect) { + maskDrawable?.bounds = bounds + } + } + + /** + * Translation coordinates so that the mask is drawn at the right location + * on the screen. + */ + private val maskOffset = PointF() + + /** + * Clipping path that includes the dimensions and corners for this view. + */ + private val path = Path() + + private val rect = RectF() + + private var gradientColors: ChatColors? = null + private var corners: FloatArray = floatArrayOf() + private var fillColor: Int = 0 + + override fun draw(canvas: Canvas) { + if (gradientColors == null && fillColor == 0) { + return + } + + val mask = maskDrawable + if (gradientColors != null && mask != null) { + canvas.withTranslation(-maskOffset.x, -maskOffset.y) { + canvas.withClip(path) { + mask.draw(canvas) + } + } + } else { + path.reset() + rect.set(bounds) + path.addRoundRect(rect, corners, Path.Direction.CW) + canvas.withClip(path) { + canvas.drawColor(fillColor) + } + } + } + + override fun setAlpha(alpha: Int) = Unit + + override fun setColorFilter(colorFilter: ColorFilter?) = Unit + + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + /** + * Applies the given [Projection] as the clipping path for the canvas on subsequent draws. + * Also applies the given [Projection]'s (x,y) (Top, Left) coordinates as the mask offset, + * which is used as a canvas translation before drawing. + * + * This is done separately from setting the corners and color, because it needs to happen + * on every frame we would normally perform a decorator onDraw, whereas setting the corners + * and color only needs to happen on bind. + */ + fun applyMaskProjection(projection: Projection) { + path.reset() + projection.applyToPath(path) + + maskOffset.set( + projection.x, + projection.y + ) + + invalidateSelf() + } + + fun isSolidColor(): Boolean { + return gradientColors == null + } + + /** + * Sets the chat color and shape as specified. If the colors are a gradient, + * we will use masking to draw, and we will draw every time we're told to by + * the decorator. + * + * If a solid color is set, we can skip drawing as we move, since we haven't changed. + */ + fun setChatColors( + chatColors: ChatColors, + corners: Corners + ) { + this.gradientColors = chatColors + this.corners = corners.toRadii() + + if (chatColors.isGradient()) { + if (maskDrawable == null) { + maskDrawable = chatColors.chatBubbleMask + } + + this.fillColor = 0 + } else { + this.fillColor = chatColors.asSingleColor() + this.gradientColors = null + } + + invalidateSelf() + } + + private object ChatColorsItemDecoration : RecyclerView.ItemDecoration() { + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + parent.children.map { parent.getChildViewHolder(it) }.filterIsInstance().forEach { element -> + element.invalidateChatColorsDrawable(parent) + } + } + } + + interface ChatColorsDrawableInvalidator { + fun invalidateChatColorsDrawable(coordinateRoot: ViewGroup) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/InteractiveConversationElement.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/InteractiveConversationElement.kt index 739f95a009..c2ad606263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/InteractiveConversationElement.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/InteractiveConversationElement.kt @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.util.ProjectionList /** * A conversation element that a user can either swipe or snapshot */ -interface InteractiveConversationElement { +interface InteractiveConversationElement : ChatColorsDrawable.ChatColorsDrawableInvalidator { val conversationMessage: ConversationMessage val root: ViewGroup diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShape.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShape.kt index 07615dc5da..f077e101bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShape.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShape.kt @@ -5,8 +5,6 @@ package org.thoughtcrime.securesms.conversation.v2.items -import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.shape.ShapeAppearanceModel import org.signal.core.util.dp import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.util.DateUtils @@ -36,11 +34,6 @@ class V2ConversationItemShape( var corners: Projection.Corners = Projection.Corners(bigRadius) private set - var bodyBubble: MaterialShapeDrawable = MaterialShapeDrawable( - ShapeAppearanceModel.Builder().setAllCornerSizes(bigRadius).build() - ) - private set - /** * Sets the message spacing and corners based off the given information. This * updates the class state. @@ -93,12 +86,6 @@ class V2ConversationItemShape( } corners = newCorners - bodyBubble.shapeAppearanceModel = ShapeAppearanceModel.builder() - .setTopLeftCornerSize(corners.topLeft) - .setTopRightCornerSize(corners.topRight) - .setBottomLeftCornerSize(corners.bottomLeft) - .setBottomRightCornerSize(corners.bottomRight) - .build() } private fun isSingularMessage( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt index 765c7ea0c8..63564942cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt @@ -63,7 +63,7 @@ fun V2ConversationItemTextOnlyIncomingBinding.bridge(): V2ConversationItemTextOn conversationItemFooterBackground = conversationItemFooterBackground, conversationItemAlert = null, conversationItemFooterSpace = null, - isIncoming = false + isIncoming = true ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt index 27aeaed1b3..f17c078842 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt @@ -6,7 +6,6 @@ package org.thoughtcrime.securesms.conversation.v2.items import android.content.Context -import android.content.res.ColorStateList import android.graphics.Color import androidx.annotation.ColorInt import androidx.core.content.ContextCompat @@ -64,30 +63,30 @@ class V2ConversationItemTheme( ) } + @ColorInt fun getBodyBubbleColor( conversationMessage: ConversationMessage - ): ColorStateList { + ): Int { if (conversationMessage.messageRecord.hasNoBubble(context)) { - return ColorStateList.valueOf(Color.TRANSPARENT) + return Color.TRANSPARENT } return getFooterBubbleColor(conversationMessage) } + @ColorInt fun getFooterBubbleColor( conversationMessage: ConversationMessage - ): ColorStateList { - return ColorStateList.valueOf( - if (conversationMessage.messageRecord.isOutgoing) { - Color.TRANSPARENT + ): Int { + return if (conversationMessage.messageRecord.isOutgoing) { + Color.TRANSPARENT + } else { + if (conversationContext.hasWallpaper()) { + ContextCompat.getColor(context, R.color.signal_colorSurface) } else { - if (conversationContext.hasWallpaper()) { - ContextCompat.getColor(context, R.color.signal_colorSurface) - } else { - ContextCompat.getColor(context, R.color.signal_colorSurface2) - } + ContextCompat.getColor(context, R.color.signal_colorSurface2) } - ) + } } @ColorInt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemViewHolder.kt index 43bd8e2c38..9757eb3700 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemViewHolder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemViewHolder.kt @@ -23,8 +23,6 @@ import android.view.ViewGroup.MarginLayoutParams import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.shape.ShapeAppearanceModel import org.signal.core.util.StringUtil import org.signal.core.util.dp import org.thoughtcrime.securesms.R @@ -32,6 +30,7 @@ import org.thoughtcrime.securesms.components.mention.MentionAnnotation import org.thoughtcrime.securesms.conversation.BodyBubbleLayoutTransition import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode import org.thoughtcrime.securesms.conversation.ConversationMessage +import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable @@ -55,7 +54,6 @@ import org.thoughtcrime.securesms.util.VibrateUtil import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder import org.thoughtcrime.securesms.util.hasExtraText -import org.thoughtcrime.securesms.util.hasNoBubble import org.thoughtcrime.securesms.util.isScheduled import org.thoughtcrime.securesms.util.visible import java.util.Locale @@ -82,6 +80,9 @@ class V2TextOnlyViewHolder>( companion object { private val STYLE_FACTORY = StyleFactory { arrayOf(BackgroundColorSpan(Color.YELLOW), ForegroundColorSpan(Color.BLACK)) } private const val CONDENSED_MODE_MAX_LINES = 3 + + private val footerCorners = Projection.Corners(18f.dp) + private val transparentChatColors = ChatColors.forColor(ChatColors.Id.NotSet, Color.TRANSPARENT) } private var messageId: Long = Long.MAX_VALUE @@ -89,13 +90,6 @@ class V2TextOnlyViewHolder>( private val projections = ProjectionList() private val footerDelegate = V2FooterPositionDelegate(binding) - private val conversationItemFooterBackgroundCorners = Projection.Corners(18f.dp) - private val conversationItemFooterBackground = MaterialShapeDrawable( - ShapeAppearanceModel.Builder() - .setAllCornerSizes(18f.dp) - .build() - ) - override lateinit var conversationMessage: ConversationMessage override val root: ViewGroup = binding.root @@ -117,6 +111,9 @@ class V2TextOnlyViewHolder>( private var reactionMeasureListener: ReactionMeasureListener = ReactionMeasureListener() + private val bodyBubbleDrawable = ChatColorsDrawable() + private val footerDrawable = ChatColorsDrawable() + init { binding.root.addOnMeasureListener(footerDelegate) @@ -150,7 +147,15 @@ class V2TextOnlyViewHolder>( binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25)) } + binding.conversationItemBodyWrapper.background = bodyBubbleDrawable binding.conversationItemBodyWrapper.layoutTransition = BodyBubbleLayoutTransition() + + binding.conversationItemFooterBackground.background = footerDrawable + } + + override fun invalidateChatColorsDrawable(coordinateRoot: ViewGroup) { + invalidateBodyBubbleDrawable(coordinateRoot) + invalidateFooterDrawable(coordinateRoot) } override fun bind(model: Model) { @@ -164,11 +169,6 @@ class V2TextOnlyViewHolder>( adapterPosition = bindingAdapterPosition ) - shapeDelegate.bodyBubble.fillColor = themeDelegate.getBodyBubbleColor(conversationMessage) - - binding.conversationItemBodyWrapper.background = shapeDelegate.bodyBubble - binding.conversationItemReply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor()) - presentBody() presentDate(shape) presentDeliveryStatus(shape) @@ -178,6 +178,19 @@ class V2TextOnlyViewHolder>( presentSender() presentReactions() + bodyBubbleDrawable.setChatColors( + if (binding.conversationItemBody.isJumbomoji) { + transparentChatColors + } else if (binding.isIncoming) { + ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getBodyBubbleColor(conversationMessage)) + } else { + conversationMessage.threadRecipient.chatColors + }, + shapeDelegate.corners + ) + + binding.conversationItemReply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor()) + itemView.updateLayoutParams { topMargin = shape.topPadding.toInt() bottomMargin = shape.bottomPadding.toInt() @@ -208,29 +221,13 @@ class V2TextOnlyViewHolder>( return projections } + /** + * Note: This is not necessary for CFV2 Text-Only items because the background is rendered by + * [ChatColorsDrawable] + */ override fun getColorizerProjections(coordinateRoot: ViewGroup): ProjectionList { projections.clear() - if (conversationMessage.messageRecord.isOutgoing) { - if (!conversationMessage.messageRecord.hasNoBubble(context)) { - projections.add( - Projection.relativeToParent( - coordinateRoot, - binding.conversationItemBodyWrapper, - shapeDelegate.corners - ).translateX(binding.conversationItemBodyWrapper.translationX).translateY(root.translationY) - ) - } else if (conversationContext.hasWallpaper()) { - projections.add( - Projection.relativeToParent( - coordinateRoot, - binding.conversationItemFooterBackground, - conversationItemFooterBackgroundCorners - ).translateX(binding.conversationItemFooterBackground.translationX).translateY(root.translationY) - ) - } - } - return projections } @@ -277,6 +274,36 @@ class V2TextOnlyViewHolder>( override fun shouldProjectContent(): Boolean = false + private fun invalidateFooterDrawable(coordinateRoot: ViewGroup) { + if (footerDrawable.isSolidColor()) { + return + } + + val projection = Projection.relativeToParent( + coordinateRoot, + binding.conversationItemFooterBackground, + shapeDelegate.corners + ) + + footerDrawable.applyMaskProjection(projection) + projection.release() + } + + private fun invalidateBodyBubbleDrawable(coordinateRoot: ViewGroup) { + if (bodyBubbleDrawable.isSolidColor()) { + return + } + + val projection = Projection.relativeToParent( + coordinateRoot, + binding.conversationItemBodyWrapper, + shapeDelegate.corners + ) + + bodyBubbleDrawable.applyMaskProjection(projection) + projection.release() + } + private fun MessageRecord.buildMessageId(): Long { return if (isMms) -id else id } @@ -472,8 +499,14 @@ class V2TextOnlyViewHolder>( } binding.conversationItemFooterBackground.visible = true - binding.conversationItemFooterBackground.background = conversationItemFooterBackground - conversationItemFooterBackground.fillColor = themeDelegate.getFooterBubbleColor(conversationMessage) + footerDrawable.setChatColors( + if (binding.isIncoming) { + ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getFooterBubbleColor(conversationMessage)) + } else { + conversationMessage.threadRecipient.chatColors + }, + footerCorners + ) } private fun presentDate(shape: V2ConversationItemShape.MessageShape) {