Add the groundwork for the ConversationItemV2 Media item.

This commit is contained in:
Alex Hart 2023-08-29 13:52:53 -03:00 committed by Greyson Parrelli
parent f9ab5d4013
commit 75b81a0fd2
16 changed files with 787 additions and 39 deletions

View file

@ -623,6 +623,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
viewModel.setUseConversationItemV2(!state.useConversationItemV2)
}
)
switchPref(
title = DSLSettingsText.from("Use V2 ConversationItem for Media"),
isChecked = state.useConversationItemV2ForMedia,
onClick = {
viewModel.setUseConversationItemV2Media(!state.useConversationItemV2ForMedia)
}
)
}
}

View file

@ -22,5 +22,6 @@ data class InternalSettingsState(
val disableStorageService: Boolean,
val canClearOnboardingState: Boolean,
val pnpInitialized: Boolean,
val useConversationItemV2: Boolean
val useConversationItemV2: Boolean,
val useConversationItemV2ForMedia: Boolean
)

View file

@ -109,6 +109,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setUseConversationItemV2Media(enabled: Boolean) {
SignalStore.internalValues().setUseConversationItemV2Media(enabled)
refresh()
}
fun addSampleReleaseNote() {
repository.addSampleReleaseNote()
}
@ -136,7 +141,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
disableStorageService = SignalStore.internalValues().storageServiceDisabled(),
canClearOnboardingState = SignalStore.storyValues().hasDownloadedOnboardingStory && Stories.isFeatureEnabled(),
pnpInitialized = SignalStore.misc().hasPniInitializedDevices(),
useConversationItemV2 = SignalStore.internalValues().useConversationItemV2()
useConversationItemV2 = SignalStore.internalValues().useConversationItemV2(),
useConversationItemV2ForMedia = SignalStore.internalValues().useConversationItemV2Media()
)
fun onClearOnboardingState() {

View file

@ -36,9 +36,12 @@ import org.thoughtcrime.securesms.conversation.v2.data.OutgoingMedia
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
import org.thoughtcrime.securesms.conversation.v2.data.ThreadHeader
import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationContext
import org.thoughtcrime.securesms.conversation.v2.items.V2TextOnlyViewHolder
import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemMediaViewHolder
import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemTextOnlyViewHolder
import org.thoughtcrime.securesms.conversation.v2.items.bridge
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.databinding.V2ConversationItemMediaIncomingBinding
import org.thoughtcrime.securesms.databinding.V2ConversationItemMediaOutgoingBinding
import org.thoughtcrime.securesms.databinding.V2ConversationItemTextOnlyIncomingBinding
import org.thoughtcrime.securesms.databinding.V2ConversationItemTextOnlyOutgoingBinding
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer
@ -93,25 +96,37 @@ class ConversationAdapterV2(
ConversationUpdateViewHolder(view)
}
registerFactory(OutgoingMedia::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_sent_multimedia, parent, false)
OutgoingMediaViewHolder(view)
}
if (SignalStore.internalValues().useConversationItemV2Media()) {
registerFactory(OutgoingMedia::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.v2_conversation_item_media_outgoing, parent, false)
V2ConversationItemMediaViewHolder(V2ConversationItemMediaOutgoingBinding.bind(view).bridge(), this)
}
registerFactory(IncomingMedia::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_received_multimedia, parent, false)
IncomingMediaViewHolder(view)
registerFactory(IncomingMedia::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.v2_conversation_item_media_incoming, parent, false)
V2ConversationItemMediaViewHolder(V2ConversationItemMediaIncomingBinding.bind(view).bridge(), this)
}
} else {
registerFactory(OutgoingMedia::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_sent_multimedia, parent, false)
OutgoingMediaViewHolder(view)
}
registerFactory(IncomingMedia::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_received_multimedia, parent, false)
IncomingMediaViewHolder(view)
}
}
if (SignalStore.internalValues().useConversationItemV2()) {
registerFactory(OutgoingTextOnly::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.v2_conversation_item_text_only_outgoing, parent, false)
V2TextOnlyViewHolder(V2ConversationItemTextOnlyOutgoingBinding.bind(view).bridge(), this)
V2ConversationItemTextOnlyViewHolder(V2ConversationItemTextOnlyOutgoingBinding.bind(view).bridge(), this)
}
registerFactory(IncomingTextOnly::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.v2_conversation_item_text_only_incoming, parent, false)
V2TextOnlyViewHolder(V2ConversationItemTextOnlyIncomingBinding.bind(view).bridge(), this)
V2ConversationItemTextOnlyViewHolder(V2ConversationItemTextOnlyIncomingBinding.bind(view).bridge(), this)
}
} else {
registerFactory(OutgoingTextOnly::class.java) { parent ->

View file

@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Outline
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.PointF
@ -14,6 +15,7 @@ import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.view.ViewGroup
import androidx.core.graphics.toRectF
import androidx.core.graphics.withClip
import androidx.core.graphics.withTranslation
import androidx.core.view.children
@ -31,6 +33,7 @@ class ChatColorsDrawable : Drawable() {
companion object {
private var maskDrawable: Drawable? = null
private var latestBounds: Rect? = null
/**
* Binds the ChatColorsDrawable static cache to the lifecycle of the given recycler-view
@ -47,6 +50,7 @@ class ChatColorsDrawable : Drawable() {
}
private fun applyBounds(bounds: Rect) {
latestBounds = bounds
maskDrawable?.bounds = bounds
}
}
@ -65,7 +69,7 @@ class ChatColorsDrawable : Drawable() {
private val rect = RectF()
private var gradientColors: ChatColors? = null
private var corners: FloatArray = floatArrayOf()
private var corners: FloatArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
private var fillColor: Int = 0
override fun draw(canvas: Canvas) {
@ -98,6 +102,21 @@ class ChatColorsDrawable : Drawable() {
return PixelFormat.TRANSLUCENT
}
/**
* Note: APIs had the wrong name for setPath here, so we have to use the deprecated method.
*/
@Suppress("DEPRECATION")
override fun getOutline(outline: Outline) {
val path = Path()
path.addRoundRect(
bounds.toRectF(),
corners,
Path.Direction.CW
)
outline.setConvexPath(path)
}
/**
* 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,
@ -134,15 +153,20 @@ class ChatColorsDrawable : Drawable() {
chatColors: ChatColors,
corners: Corners
) {
this.gradientColors = chatColors
this.corners = corners.toRadii()
if (chatColors.isGradient()) {
if (maskDrawable == null) {
maskDrawable = chatColors.chatBubbleMask
val maskBounds = latestBounds
if (maskBounds != null) {
maskDrawable?.bounds = maskBounds
}
}
this.fillColor = 0
this.gradientColors = chatColors
} else {
this.fillColor = chatColors.asSingleColor()
this.gradientColors = null

View file

@ -0,0 +1,78 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2.items
import android.widget.ImageView
import org.thoughtcrime.securesms.databinding.V2ConversationItemMediaIncomingBinding
import org.thoughtcrime.securesms.databinding.V2ConversationItemMediaOutgoingBinding
import org.thoughtcrime.securesms.util.views.Stub
/**
* Pass-through interface for bridging incoming and outgoing media message views.
*
* Essentially, just a convenience wrapper since the layouts differ *very slightly* and
* we want to be able to have each follow the same code-path.
*/
data class V2ConversationItemMediaBindingBridge(
val textBridge: V2ConversationItemTextOnlyBindingBridge,
val thumbnailStub: Stub<ImageView>
)
/**
* Wraps the binding in the bridge.
*/
fun V2ConversationItemMediaIncomingBinding.bridge(): V2ConversationItemMediaBindingBridge {
val textBridge = V2ConversationItemTextOnlyBindingBridge(
root = root,
senderName = groupMessageSender,
senderPhoto = contactPhoto,
senderBadge = badge,
conversationItemBody = conversationItemBody,
conversationItemBodyWrapper = conversationItemBodyWrapper,
conversationItemReply = conversationItemReply,
conversationItemReactions = conversationItemReactions,
conversationItemDeliveryStatus = null,
conversationItemFooterDate = conversationItemFooterDate,
conversationItemFooterExpiry = conversationItemExpirationTimer,
conversationItemFooterBackground = conversationItemFooterBackground,
conversationItemAlert = null,
conversationItemFooterSpace = null,
isIncoming = true
)
return V2ConversationItemMediaBindingBridge(
textBridge = textBridge,
thumbnailStub = Stub(conversationItemThumbnailStub)
)
}
/**
* Wraps the binding in the bridge.
*/
fun V2ConversationItemMediaOutgoingBinding.bridge(): V2ConversationItemMediaBindingBridge {
val textBridge = V2ConversationItemTextOnlyBindingBridge(
root = root,
senderName = null,
senderPhoto = null,
senderBadge = null,
conversationItemBody = conversationItemBody,
conversationItemBodyWrapper = conversationItemBodyWrapper,
conversationItemReply = conversationItemReply,
conversationItemReactions = conversationItemReactions,
conversationItemDeliveryStatus = conversationItemDeliveryStatus,
conversationItemFooterDate = conversationItemFooterDate,
conversationItemFooterExpiry = conversationItemExpirationTimer,
conversationItemFooterBackground = conversationItemFooterBackground,
conversationItemAlert = conversationItemAlert,
conversationItemFooterSpace = footerEndPad,
isIncoming = false
)
return V2ConversationItemMediaBindingBridge(
textBridge = textBridge,
thumbnailStub = Stub(conversationItemThumbnailStub)
)
}

View file

@ -0,0 +1,198 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2.items
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import androidx.core.view.updateLayoutParams
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.transition.Transition
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.changeConstraints
/**
* Represents a media-backed conversation item.
*/
class V2ConversationItemMediaViewHolder<Model : MappingModel<Model>>(
private val binding: V2ConversationItemMediaBindingBridge,
private val conversationContext: V2ConversationContext
) : V2ConversationItemTextOnlyViewHolder<Model>(
binding.textBridge,
conversationContext,
V2FooterPositionDelegate(binding)
) {
private var thumbnailSlide: Slide? = null
private var placeholderTarget: PlaceholderTarget? = null
private val thumbnailSize = intArrayOf(0, 0)
init {
binding.textBridge.conversationItemBodyWrapper.clipToOutline = true
}
override fun bind(model: Model) {
conversationMessage = (model as ConversationMessageElement).conversationMessage
presentThumbnail()
super.bind(model)
}
private fun presentThumbnail() {
val slideDeck = requireMediaMessage().slideDeck
if (slideDeck.thumbnailSlides.isEmpty() || slideDeck.thumbnailSlides.size > 1) {
binding.thumbnailStub.visibility = View.GONE
thumbnailSize[0] = -1
thumbnailSize[1] = -1
binding.textBridge.root.changeConstraints {
this.constrainMaxWidth(binding.textBridge.conversationItemBodyWrapper.id, 0)
}
return
}
binding.thumbnailStub.visibility = View.VISIBLE
val thumbnail = slideDeck.thumbnailSlides.first()
// TODO [alex] -- Is this correct?
if (thumbnail == thumbnailSlide) {
return
}
thumbnailSlide = thumbnail
conversationContext.glideRequests.clear(binding.thumbnailStub.get())
if (placeholderTarget != null) {
conversationContext.glideRequests.clear(placeholderTarget)
}
// endif
val thumbnailUri = thumbnail.uri
val thumbnailBlur = thumbnail.placeholderBlur
val thumbnailAttachment = thumbnail.asAttachment()
val thumbnailWidth = thumbnailAttachment.width
val thumbnailHeight = thumbnailAttachment.height
val maxWidth = context.resources.getDimensionPixelSize(R.dimen.media_bubble_max_width)
val maxHeight = context.resources.getDimensionPixelSize(R.dimen.media_bubble_max_height)
setThumbnailSize(
thumbnailWidth,
thumbnailHeight,
maxWidth,
maxHeight
)
binding.thumbnailStub.get().updateLayoutParams {
width = thumbnailSize[0]
height = thumbnailSize[1]
}
binding.textBridge.root.changeConstraints {
this.constrainMaxWidth(binding.textBridge.conversationItemBodyWrapper.id, thumbnailSize[0])
}
if (thumbnailBlur != null) {
val placeholderTarget = PlaceholderTarget(binding.thumbnailStub.get())
conversationContext
.glideRequests
.load(thumbnailBlur)
.centerInside()
.dontAnimate()
.override(thumbnailSize[0], thumbnailSize[1])
.into(placeholderTarget)
this.placeholderTarget = placeholderTarget
}
if (thumbnailUri != null) {
conversationContext
.glideRequests
.load(DecryptableStreamUriLoader.DecryptableUri(thumbnailUri))
.centerInside()
.dontAnimate()
.override(thumbnailSize[0], thumbnailSize[1])
.into(binding.thumbnailStub.get())
}
}
private fun setThumbnailSize(
thumbnailWidth: Int,
thumbnailHeight: Int,
maxWidth: Int,
maxHeight: Int
) {
if (thumbnailWidth == 0 || thumbnailHeight == 0) {
thumbnailSize[0] = maxWidth
thumbnailSize[1] = maxHeight
return
}
if (thumbnailWidth <= maxWidth && thumbnailHeight <= maxHeight) {
thumbnailSize[0] = thumbnailWidth
thumbnailSize[1] = thumbnailHeight
return
}
if (thumbnailWidth > maxWidth) {
val thumbnailScale = 1 - ((thumbnailWidth - maxWidth) / thumbnailWidth.toFloat())
thumbnailSize[0] = (thumbnailWidth * thumbnailScale).toInt()
thumbnailSize[1] = (thumbnailHeight * thumbnailScale).toInt()
}
if (isThumbnailMetricsSatisfied(maxWidth, maxHeight)) {
return
}
if (thumbnailHeight > maxHeight) {
val thumbnailScale = 1 - ((thumbnailHeight - maxHeight) / thumbnailHeight.toFloat())
thumbnailSize[0] = (thumbnailWidth * thumbnailScale).toInt()
thumbnailSize[1] = (thumbnailHeight * thumbnailScale).toInt()
}
if (isThumbnailMetricsSatisfied(maxWidth, maxHeight)) {
return
}
setThumbnailSize(
thumbnailSize[0],
thumbnailSize[1],
maxWidth,
maxHeight
)
}
private fun isThumbnailMetricsSatisfied(maxWidth: Int, maxHeight: Int): Boolean {
return thumbnailSize[0] in 1..maxWidth && thumbnailSize[1] in 1..maxHeight
}
private fun requireMediaMessage(): MediaMmsMessageRecord {
return conversationMessage.messageRecord as MediaMmsMessageRecord
}
private inner class PlaceholderTarget(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
override fun onLoadFailed(errorDrawable: Drawable?) {
view.background = errorDrawable
}
override fun onResourceCleared(placeholder: Drawable?) {
view.background = placeholder
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
view.background = resource
}
}
}

View file

@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.util.visible
* Responsible for drawing the conversation bubble when a user long-presses it and the reaction
* overlay appears.
*/
class V2TextOnlySnapshotStrategy(
class V2ConversationItemSnapshotStrategy(
private val binding: V2ConversationItemTextOnlyBindingBridge
) : InteractiveConversationElement.SnapshotStrategy {

View file

@ -57,9 +57,10 @@ import java.util.Locale
/**
* Represents a text-only conversation item.
*/
class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
private val binding: V2ConversationItemTextOnlyBindingBridge,
private val conversationContext: V2ConversationContext
private val conversationContext: V2ConversationContext,
footerDelegate: V2FooterPositionDelegate = V2FooterPositionDelegate(binding)
) : V2ConversationItemViewHolder<Model>(binding.root, conversationContext), Multiselectable, InteractiveConversationElement {
companion object {
@ -73,7 +74,6 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
private var messageId: Long = Long.MAX_VALUE
private val projections = ProjectionList()
private val footerDelegate = V2FooterPositionDelegate(binding)
private val dispatchTouchEventListener = V2OnDispatchTouchEventListener(conversationContext, binding)
override lateinit var conversationMessage: ConversationMessage
@ -212,7 +212,7 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
}
override fun getSnapshotStrategy(): InteractiveConversationElement.SnapshotStrategy {
return V2TextOnlySnapshotStrategy(binding)
return V2ConversationItemSnapshotStrategy(binding)
}
/**
@ -324,7 +324,10 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
binding.conversationItemBody.maxLines = Integer.MAX_VALUE
}
binding.conversationItemBody.text = StringUtil.trim(styledText)
val bodyText = StringUtil.trim(styledText)
binding.conversationItemBody.visible = bodyText.isNotEmpty()
binding.conversationItemBody.text = bodyText
}
private fun linkifyMessageBody(messageBody: Spannable) {

View file

@ -6,11 +6,13 @@
package org.thoughtcrime.securesms.conversation.v2.items
import android.view.View
import android.widget.ImageView
import org.signal.core.util.dp
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.padding
import org.thoughtcrime.securesms.util.views.Stub
import org.thoughtcrime.securesms.util.visible
import kotlin.math.max
@ -18,15 +20,14 @@ import kotlin.math.max
* Logical delegate for determining the footer position for a particular conversation item.
*/
class V2FooterPositionDelegate private constructor(
private val isIncoming: Boolean,
private val root: V2ConversationItemLayout,
private val footerViews: List<View>,
private val bodyContainer: View,
private val body: EmojiTextView
private val body: EmojiTextView,
private val thumbnailView: Stub<ImageView>?
) : V2ConversationItemLayout.OnMeasureListener {
constructor(binding: V2ConversationItemTextOnlyBindingBridge) : this(
binding.isIncoming,
binding.root,
listOfNotNull(
binding.conversationItemFooterDate,
@ -35,7 +36,21 @@ class V2FooterPositionDelegate private constructor(
binding.conversationItemFooterSpace
),
binding.conversationItemBodyWrapper,
binding.conversationItemBody
binding.conversationItemBody,
null
)
constructor(binding: V2ConversationItemMediaBindingBridge) : this(
binding.textBridge.root,
listOfNotNull(
binding.textBridge.conversationItemFooterDate,
binding.textBridge.conversationItemDeliveryStatus,
binding.textBridge.conversationItemFooterExpiry,
binding.textBridge.conversationItemFooterSpace
),
binding.textBridge.conversationItemBodyWrapper,
binding.textBridge.conversationItemBody,
binding.thumbnailStub
)
private val gutters = 48.dp + 16.dp
@ -48,7 +63,12 @@ class V2FooterPositionDelegate private constructor(
}
override fun onPostMeasure(): Boolean {
val maxWidth = root.measuredWidth - gutters
val maxWidth = if (thumbnailView?.isVisible == true) {
thumbnailView.get().layoutParams.width
} else {
root.measuredWidth - gutters
}
val lastLineWidth = body.lastLineWidth
val footerWidth = getFooterWidth()
@ -81,7 +101,7 @@ class V2FooterPositionDelegate private constructor(
return
}
bodyContainer.padding(right = 0, left = 0, bottom = footerViews.first().measuredHeight)
body.padding(right = 0, left = 0, bottom = footerViews.first().measuredHeight)
displayState = DisplayState.UNDERNEATH
}
@ -90,7 +110,7 @@ class V2FooterPositionDelegate private constructor(
return
}
val targetWidth = body.measuredWidth + getFooterWidth()
val targetWidth = body.measuredWidth + 24.dp + getFooterWidth()
val end = max(0, targetWidth - bodyContainer.measuredWidth) - 8.dp
val (left, right) = if (bodyContainer.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
0 to end
@ -98,7 +118,7 @@ class V2FooterPositionDelegate private constructor(
end to 0
}
bodyContainer.padding(right = right, left = left, bottom = 0)
body.padding(right = right, left = left, bottom = 0)
displayState = DisplayState.END
}
@ -107,7 +127,7 @@ class V2FooterPositionDelegate private constructor(
return
}
bodyContainer.padding(right = 0, left = 0, bottom = 0)
body.padding(right = 0, left = 0, bottom = 0)
displayState = DisplayState.TUCKED
}

View file

@ -30,6 +30,7 @@ public final class InternalValues extends SignalStoreValues {
public static final String FORCE_WEBSOCKET_MODE = "internal.force_websocket_mode";
public static final String LAST_SCROLL_POSITION = "internal.last_scroll_position";
public static final String CONVERSATION_ITEM_V2 = "internal.conversation_item_v2";
public static final String CONVERSATION_ITEM_V2_MEDIA = "internal.conversation_item_v2_media";
InternalValues(KeyValueStore store) {
super(store);
@ -198,4 +199,12 @@ public final class InternalValues extends SignalStoreValues {
public boolean useConversationItemV2() {
return FeatureFlags.internalUser() && getBoolean(CONVERSATION_ITEM_V2, false);
}
public void setUseConversationItemV2Media(boolean useConversationFragmentV2Media) {
putBoolean(CONVERSATION_ITEM_V2_MEDIA, useConversationFragmentV2Media);
}
public boolean useConversationItemV2Media() {
return FeatureFlags.internalUser() && getBoolean(CONVERSATION_ITEM_V2_MEDIA, false);
}
}

View file

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:focusable="true"
android:nextFocusLeft="@+id/container"
android:nextFocusRight="@+id/embedded_text_editor">
<!-- STR Icon -->
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversation_item_reply"
android:layout_width="@dimen/conversation_item_reply_size"
android:layout_height="@dimen/conversation_item_reply_size"
android:alpha="0"
android:tint="@color/signal_icon_tint_secondary"
app:contentPadding="9dp"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_body_wrapper"
app:layout_constraintStart_toStartOf="@id/conversation_item_body_wrapper"
app:layout_constraintTop_toTopOf="@id/conversation_item_body_wrapper"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle"
app:srcCompat="@drawable/symbol_reply_24" />
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/contact_photo"
android:layout_width="@dimen/conversation_item_avatar_size"
android:layout_height="@dimen/conversation_item_avatar_size"
android:layout_marginStart="12dp"
android:contentDescription="@string/conversation_item_received__contact_photo_description"
android:cropToPadding="true"
android:visibility="gone"
app:fallbackImageSize="small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/badge"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="14dp"
android:layout_marginTop="16dp"
android:visibility="gone"
app:badge_size="small"
app:layout_constraintStart_toStartOf="@id/contact_photo"
app:layout_constraintTop_toTopOf="@id/contact_photo"
tools:visibility="visible" />
<!-- Body -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/conversation_item_body_wrapper"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="48dp"
android:orientation="vertical"
app:cardElevation="0dp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/contact_photo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
app:layout_goneMarginStart="16dp"
tools:background="@color/black">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/group_message_sender"
style="@style/TextAppearance.Signal.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:layout_marginEnd="4sp"
android:layout_marginBottom="-6dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="@dimen/message_bubble_horizontal_padding"
android:paddingEnd="@dimen/message_bubble_horizontal_padding"
android:textColor="@color/signal_text_primary"
android:textStyle="bold"
app:layout_constraintBaseline_toTopOf="@id/conversation_item_thumbnail_stub"
app:layout_constraintEnd_toEndOf="@id/conversation_item_body"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="+14152222222"
tools:visibility="visible" />
<!-- Media content goes here -->
<ViewStub
android:id="@+id/conversation_item_thumbnail_stub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout="@layout/v2_conversation_item_thumbnail_stub"
app:layout_constraintBottom_toTopOf="@id/conversation_item_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/group_message_sender"
app:layout_goneMarginTop="0dp" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/conversation_item_sent_text_primary_color"
android:textColorLink="@color/conversation_item_sent_text_primary_color"
android:textSize="16sp"
app:emoji_maxLength="1000"
app:emoji_renderMentions="true"
app:emoji_renderSpoilers="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conversation_item_thumbnail_stub"
app:layout_constraintWidth_default="wrap"
app:measureLastLine="true"
app:scaleEmojis="true"
tools:text="Testy test test test" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Footer -->
<View
android:id="@+id/conversation_item_footer_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="-12dp"
android:layout_marginEnd="-12dp"
android:layout_marginBottom="4dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_footer_date"
app:layout_constraintEnd_toEndOf="@id/conversation_item_expiration_timer"
app:layout_constraintStart_toStartOf="@id/conversation_item_footer_date"
app:layout_constraintTop_toTopOf="@id/conversation_item_footer_date"
tools:background="@color/blue_500"
tools:visibility="visible" />
<TextView
android:id="@+id/conversation_item_footer_date"
style="@style/Signal.Text.Caption.MessageSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:autoLink="none"
android:ellipsize="end"
android:linksClickable="false"
android:maxLines="1"
android:paddingBottom="@dimen/message_bubble_bottom_padding"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_body_wrapper"
app:layout_constraintEnd_toStartOf="@id/conversation_item_expiration_timer"
app:layout_goneMarginEnd="@dimen/message_bubble_horizontal_padding"
tools:text="13:14pm" />
<org.thoughtcrime.securesms.components.ExpirationTimerView
android:id="@+id/conversation_item_expiration_timer"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_footer_date"
app:layout_constraintEnd_toEndOf="@id/conversation_item_body_wrapper"
app:layout_constraintTop_toTopOf="@id/conversation_item_footer_date" />
<!-- End Footer -->
<!-- Replies Icon -->
<!-- Reactions -->
<org.thoughtcrime.securesms.reactions.ReactionsConversationView
android:id="@+id/conversation_item_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:layout_marginEnd="5dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="@id/conversation_item_body_wrapper"
app:layout_constraintTop_toBottomOf="@id/conversation_item_body_wrapper"
app:rcv_outgoing="false" />
</org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemLayout>

View file

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:focusable="true"
android:nextFocusLeft="@+id/container"
android:nextFocusRight="@+id/embedded_text_editor">
<!-- STR Icon -->
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversation_item_reply"
android:layout_width="@dimen/conversation_item_reply_size"
android:layout_height="@dimen/conversation_item_reply_size"
android:alpha="0"
android:tint="@color/signal_icon_tint_secondary"
app:contentPadding="9dp"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_body_wrapper"
app:layout_constraintStart_toStartOf="@id/conversation_item_body_wrapper"
app:layout_constraintTop_toTopOf="@id/conversation_item_body_wrapper"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle"
app:srcCompat="@drawable/symbol_reply_24" />
<!-- Body -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/conversation_item_body_wrapper"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/conversation_item_alert"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
app:layout_goneMarginEnd="16dp"
tools:background="@color/black">
<!-- Media content goes here -->
<ViewStub
android:id="@+id/conversation_item_thumbnail_stub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/v2_conversation_item_thumbnail_stub"
app:layout_constraintBottom_toTopOf="@id/conversation_item_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginTop="0dp" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/conversation_item_sent_text_primary_color"
android:textColorLink="@color/conversation_item_sent_text_primary_color"
android:textSize="16sp"
app:emoji_maxLength="1000"
app:emoji_renderMentions="true"
app:emoji_renderSpoilers="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conversation_item_thumbnail_stub"
app:layout_constraintWidth_default="wrap"
app:measureLastLine="true"
app:scaleEmojis="true"
tools:text="Testy test test test" />
</androidx.constraintlayout.widget.ConstraintLayout>
<org.thoughtcrime.securesms.components.AlertView
android:id="@+id/conversation_item_alert"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Footer -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_footer_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="conversation_item_delivery_status,conversation_item_footer_date" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_footer_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="conversation_item_delivery_status,conversation_item_footer_date" />
<View
android:id="@+id/conversation_item_footer_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="-12dp"
android:layout_marginEnd="-12dp"
android:layout_marginBottom="4dp"
android:background="@color/blue_500"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/barrier_footer_bottom"
app:layout_constraintEnd_toEndOf="@id/conversation_item_delivery_status"
app:layout_constraintStart_toStartOf="@id/conversation_item_footer_date"
app:layout_constraintTop_toTopOf="@id/barrier_footer_top"
tools:visibility="visible" />
<TextView
android:id="@+id/conversation_item_footer_date"
style="@style/Signal.Text.Caption.MessageSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:autoLink="none"
android:ellipsize="end"
android:linksClickable="false"
android:maxLines="1"
android:paddingBottom="@dimen/message_bubble_bottom_padding"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_body_wrapper"
app:layout_constraintEnd_toStartOf="@id/conversation_item_expiration_timer"
tools:text="13:14pm" />
<org.thoughtcrime.securesms.components.ExpirationTimerView
android:id="@+id/conversation_item_expiration_timer"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_body_wrapper"
app:layout_constraintEnd_toStartOf="@id/conversation_item_delivery_status" />
<org.thoughtcrime.securesms.components.DeliveryStatusView
android:id="@+id/conversation_item_delivery_status"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingBottom="@dimen/message_bubble_bottom_padding"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_body_wrapper"
app:layout_constraintEnd_toStartOf="@id/footer_end_pad" />
<Space
android:id="@+id/footer_end_pad"
android:layout_width="@dimen/message_bubble_horizontal_padding"
android:layout_height="@dimen/message_bubble_horizontal_padding"
app:layout_constraintBottom_toBottomOf="@id/conversation_item_body_wrapper"
app:layout_constraintEnd_toEndOf="@id/conversation_item_body_wrapper" />
<!-- End Footer -->
<!-- Replies Icon -->
<!-- Reactions -->
<org.thoughtcrime.securesms.reactions.ReactionsConversationView
android:id="@+id/conversation_item_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="-4dp"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="@id/conversation_item_body_wrapper"
app:layout_constraintTop_toBottomOf="@id/conversation_item_body_wrapper"
app:rcv_outgoing="true" />
</org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemLayout>

View file

@ -55,7 +55,7 @@
<!-- Body -->
<LinearLayout
android:id="@+id/conversation_item_body_wrapper"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="48dp"
@ -66,6 +66,7 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/contact_photo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
app:layout_goneMarginStart="16dp"
tools:background="@color/black">
@ -90,10 +91,10 @@
android:id="@+id/conversation_item_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/message_bubble_horizontal_padding"
android:paddingTop="@dimen/message_bubble_top_padding"
android:paddingEnd="@dimen/message_bubble_horizontal_padding"
android:paddingBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/conversation_item_sent_text_primary_color"
android:textColorLink="@color/conversation_item_sent_text_primary_color"

View file

@ -30,7 +30,7 @@
<!-- Body -->
<FrameLayout
android:id="@+id/conversation_item_body_wrapper"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
app:cardElevation="0dp"
@ -39,6 +39,7 @@
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
app:layout_goneMarginEnd="16dp"
tools:background="@color/black">
@ -46,10 +47,10 @@
android:id="@+id/conversation_item_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/message_bubble_horizontal_padding"
android:paddingTop="@dimen/message_bubble_top_padding"
android:paddingEnd="@dimen/message_bubble_horizontal_padding"
android:paddingBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/conversation_item_sent_text_primary_color"
android:textColorLink="@color/conversation_item_sent_text_primary_color"

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/conversation_item_thumbnail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="false"
android:contentDescription="@string/conversation_activity__attachment_thumbnail"
android:scaleType="centerInside"
tools:viewBindingIgnore="true" />