mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-28 21:57:17 +00:00
Add the groundwork for the ConversationItemV2 Media item.
This commit is contained in:
committed by
Greyson Parrelli
parent
f9ab5d4013
commit
75b81a0fd2
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user