Split image from replies (#779)

* refactor: VisibleMessageContentView.kt re-using layouts instead of instantiating every bind to fix alignment and constraint issues for splitting thumbnails and body

* refactor: constraint works for sms only records, adjust other components of the visible message content accordingly

* feat: link previews and quotes now wrap content or align according to media type

* refactor: move back to emojitextview for body

* fix: add some padding at the bottom of the quote

* fix: voice message view not rendering properly

* fix: set visibility to false for each message content view on recycle event

* fix: untrusted attachments

* fix: compile issues and small UI improvement
This commit is contained in:
Harris 2022-01-18 14:33:04 +11:00 committed by GitHub
parent c2657bb785
commit bd5a324ad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 275 additions and 241 deletions

View File

@ -17,19 +17,14 @@ import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import kotlin.math.roundToInt
class AlbumThumbnailView : FrameLayout { class AlbumThumbnailView : FrameLayout {
@ -72,24 +67,7 @@ class AlbumThumbnailView : FrameLayout {
val rawXInt = event.rawX.toInt() val rawXInt = event.rawX.toInt()
val rawYInt = event.rawY.toInt() val rawYInt = event.rawY.toInt()
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
// Z-check in specific order
val testRect = Rect() val testRect = Rect()
// test "Read More"
binding.albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
if (testRect.contains(eventRect)) {
// dispatch to activity view
ActivityDispatcher.get(context)?.dispatchIntent { context ->
LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true)
}
return
}
val intersectedSpans = binding.albumCellBodyText.getIntersectedModalSpans(eventRect)
if (intersectedSpans.isNotEmpty()) {
intersectedSpans.forEach { span ->
span.onClick(binding.albumCellBodyText)
}
return
}
// test each album child // test each album child
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
child.getGlobalVisibleRect(testRect) child.getGlobalVisibleRect(testRect)
@ -113,6 +91,11 @@ class AlbumThumbnailView : FrameLayout {
} }
} }
fun clearViews() {
binding.albumCellContainer.removeAllViews()
slideSize = -1
}
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord, fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
isStart: Boolean, isEnd: Boolean) { isStart: Boolean, isEnd: Boolean) {
slides = message.slideDeck.thumbnailSlides slides = message.slideDeck.thumbnailSlides
@ -139,19 +122,6 @@ class AlbumThumbnailView : FrameLayout {
val thumbnailView = getThumbnailView(position) val thumbnailView = getThumbnailView(position)
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message) thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
} }
binding.albumCellBodyParent.isVisible = message.body.isNotEmpty()
val body = VisibleMessageContentView.getBodySpans(context, message, null)
binding.albumCellBodyText.text = body
post {
// post to await layout of text
binding.albumCellBodyText.layout?.let { layout ->
val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) }
?: 0
// show read more text if at least one line is ellipsized
ViewUtil.setPaddingTop(binding.albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt())
binding.albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
}
}
} }
// endregion // endregion

View File

@ -39,7 +39,12 @@ class LinkPreviewView : LinearLayout {
// endregion // endregion
// region Updating // region Updating
fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) { fun bind(
message: MmsMessageRecord,
glide: GlideRequests,
isStartOfMessageCluster: Boolean,
isEndOfMessageCluster: Boolean
) {
val linkPreview = message.linkPreviews.first() val linkPreview = message.linkPreviews.first()
url = linkPreview.url url = linkPreview.url
// Thumbnail // Thumbnail
@ -57,8 +62,7 @@ class LinkPreviewView : LinearLayout {
} }
binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme)) binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
// Body // Body
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
binding.mainLinkPreviewContainer.addView(bodyTextView)
// Corner radii // Corner radii
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopLeftRadius(cornerRadii[0])

View File

@ -47,11 +47,10 @@ class QuoteView : LinearLayout {
enum class Mode { Regular, Draft } enum class Mode { Regular, Draft }
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") } constructor(context: Context) : this(context, Mode.Regular)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") } constructor(context: Context, attrs: AttributeSet) : this(context, Mode.Regular, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
constructor(context: Context, mode: Mode) : super(context) { constructor(context: Context, mode: Mode, attrs: AttributeSet? = null) : super(context, attrs) {
this.mode = mode this.mode = mode
binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true) binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
// Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding // Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding
@ -65,7 +64,7 @@ class QuoteView : LinearLayout {
val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
// Since we're not showing the cancel button we can shorten the end margin // Since we're not showing the cancel button we can shorten the end margin
quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt() quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt()
binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams // binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
} }
} }
} }
@ -135,7 +134,7 @@ class QuoteView : LinearLayout {
if (!hasAttachments) { if (!hasAttachments) {
val accentLineLayoutParams = binding.quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams val accentLineLayoutParams = binding.quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams
accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height
binding.quoteViewAccentLine.layoutParams = accentLineLayoutParams // binding.quoteViewAccentLine.layoutParams = accentLineLayoutParams
binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
} else if (attachments != null) { } else if (attachments != null) {
binding.quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme)) binding.quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
@ -165,11 +164,11 @@ class QuoteView : LinearLayout {
} }
} }
} }
binding.mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth)) //mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth))
val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams // val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
// The start margin is different if we just show the accent line vs if we show an attachment thumbnail // The start margin is different if we just show the accent line vs if we show an attachment thumbnail
quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources) // quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources)
binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams // quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
} }
// endregion // endregion

View File

@ -10,11 +10,10 @@ import android.text.style.ForegroundColorSpan
import android.text.style.URLSpan import android.text.style.URLSpan
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -23,22 +22,20 @@ import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat import androidx.core.graphics.BlendModeCompat
import androidx.core.text.getSpans import androidx.core.text.getSpans
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.children import androidx.core.view.isVisible
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
@ -49,7 +46,7 @@ import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
private lateinit var binding: ViewVisibleMessageContentBinding private lateinit var binding: ViewVisibleMessageContentBinding
var onContentClick: ((event: MotionEvent) -> Unit)? = null var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
var onContentDoubleTap: (() -> Unit)? = null var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageContentViewDelegate? = null var delegate: VisibleMessageContentViewDelegate? = null
var indexInAdapter: Int = -1 var indexInAdapter: Int = -1
@ -74,23 +71,45 @@ class VisibleMessageContentView : LinearLayout {
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN) val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
background.colorFilter = filter background.colorFilter = filter
setBackground(background) setBackground(background)
// Body
binding.mainContainer.removeAllViews() val onlyBodyMessage = message is SmsMessageRecord
onContentClick = null val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
// reset visibilities / containers
onContentClick.clear()
binding.albumThumbnailView.clearViews()
onContentDoubleTap = null onContentDoubleTap = null
if (message.isDeleted) { if (message.isDeleted) {
val deletedMessageView = DeletedMessageView(context) binding.deletedMessageView.isVisible = true
deletedMessageView.bind(message, getTextColor(context,message)) binding.deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
binding.mainContainer.addView(deletedMessageView) return
} else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { } else {
val linkPreviewView = LinkPreviewView(context) binding.deletedMessageView.isVisible = false
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery) }
binding.mainContainer.addView(linkPreviewView)
onContentClick = { event -> linkPreviewView.calculateHit(event) } binding.quoteView.isVisible = message is MmsMessageRecord && message.quote != null
// Body text view is inside the link preview for layout convenience val quoteLayoutParams = binding.quoteView.layoutParams
} else if (message is MmsMessageRecord && message.quote != null) { quoteLayoutParams.width = if (mediaThumbnailMessage) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
binding.quoteView.layoutParams = quoteLayoutParams
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
val linkPreviewLayout = binding.linkPreviewView.layoutParams
linkPreviewLayout.width = if (mediaThumbnailMessage) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
binding.linkPreviewView.layoutParams = linkPreviewLayout
binding.untrustedView.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null
binding.voiceMessageView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
binding.documentView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
binding.openGroupInvitationView.isVisible = message.isOpenGroupInvitation
var hideBody = false
if (message is MmsMessageRecord && message.quote != null) {
binding.quoteView.isVisible = true
val quote = message.quote!! val quote = message.quote!!
val quoteView = QuoteView(context, QuoteView.Mode.Regular)
// The max content width is the max message bubble size - 2 times the horizontal padding - 2 // The max content width is the max message bubble size - 2 times the horizontal padding - 2
// times the horizontal margin. This unfortunately has to be calculated manually // times the horizontal margin. This unfortunately has to be calculated manually
// here to get the layout right. // here to get the layout right.
@ -100,88 +119,91 @@ class VisibleMessageContentView : LinearLayout {
} else { } else {
quote.text quote.text
} }
quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread, binding.quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId,
quote.isOriginalMissing, glide) quote.isOriginalMissing, glide)
binding.mainContainer.addView(quoteView) onContentClick.add { event ->
val bodyTextView = getBodyTextView(context, message, searchQuery)
ViewUtil.setPaddingTop(bodyTextView, 0)
binding.mainContainer.addView(bodyTextView)
onContentClick = { event ->
val r = Rect() val r = Rect()
quoteView.getGlobalVisibleRect(r) binding.quoteView.getGlobalVisibleRect(r)
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) { if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
delegate?.scrollToMessageIfPossible(quote.id) delegate?.scrollToMessageIfPossible(quote.id)
} else {
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
span.onClick(bodyTextView)
} }
} }
} }
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) }
// Body text view is inside the link preview for layout convenience
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) { } else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
hideBody = true
// Audio attachment // Audio attachment
if (contactIsTrusted || message.isOutgoing) { if (contactIsTrusted || message.isOutgoing) {
val voiceMessageView = VoiceMessageView(context) binding.voiceMessageView.indexInAdapter = indexInAdapter
voiceMessageView.indexInAdapter = indexInAdapter binding.voiceMessageView.delegate = context as? ConversationActivityV2
voiceMessageView.delegate = context as? ConversationActivityV2 binding.voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
binding.mainContainer.addView(voiceMessageView)
// We have to use onContentClick (rather than a click listener directly on the voice // We have to use onContentClick (rather than a click listener directly on the voice
// message view) so as to not interfere with all the other gestures. // message view) so as to not interfere with all the other gestures.
onContentClick = { voiceMessageView.togglePlayback() } onContentClick.add { binding.voiceMessageView.togglePlayback() }
onContentDoubleTap = { voiceMessageView.handleDoubleTap() } onContentDoubleTap = { binding.voiceMessageView.handleDoubleTap() }
} else { } else {
val untrustedView = UntrustedAttachmentView(context) // TODO: move this out to its own area
untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, getTextColor(context,message)) binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
binding.mainContainer.addView(untrustedView) onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
} }
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
hideBody = true
// Document attachment // Document attachment
if (contactIsTrusted || message.isOutgoing) { if (contactIsTrusted || message.isOutgoing) {
val documentView = DocumentView(context) binding.documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
documentView.bind(message, getTextColor(context, message))
binding.mainContainer.addView(documentView)
} else { } else {
val untrustedView = UntrustedAttachmentView(context) binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, getTextColor(context,message)) onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
binding.mainContainer.addView(untrustedView)
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
} }
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
// Images/Video attachment /*
* Images / Video attachment
*/
if (contactIsTrusted || message.isOutgoing) { if (contactIsTrusted || message.isOutgoing) {
val albumThumbnailView = AlbumThumbnailView(context)
binding.mainContainer.addView(albumThumbnailView)
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
// bind after add view because views are inflated and calculated during bind // bind after add view because views are inflated and calculated during bind
albumThumbnailView.bind( binding.albumThumbnailView.bind(
glideRequests = glide, glideRequests = glide,
message = message, message = message,
isStart = isStartOfMessageCluster, isStart = isStartOfMessageCluster,
isEnd = isEndOfMessageCluster isEnd = isEndOfMessageCluster
) )
onContentClick = { event -> onContentClick.add { event ->
albumThumbnailView.calculateHitObject(event, message, thread) binding.albumThumbnailView.calculateHitObject(event, message, thread)
} }
} else { } else {
val untrustedView = UntrustedAttachmentView(context) hideBody = true
untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, getTextColor(context,message)) binding.albumThumbnailView.clearViews()
binding.mainContainer.addView(untrustedView) binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
} }
} else if (message.isOpenGroupInvitation) { } else if (message.isOpenGroupInvitation) {
val openGroupInvitationView = OpenGroupInvitationView(context) hideBody = true
openGroupInvitationView.bind(message, getTextColor(context, message)) binding.openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
binding.mainContainer.addView(openGroupInvitationView) onContentClick.add { binding.openGroupInvitationView.joinOpenGroup() }
onContentClick = { openGroupInvitationView.joinOpenGroup() } }
} else {
val bodyTextView = getBodyTextView(context, message, searchQuery) binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody
binding.mainContainer.addView(bodyTextView)
onContentClick = { event -> // set it to use constraints if not only a text message, otherwise wrap content to whatever width it wants
// intersectedModalSpans should only be a list of one item val params = binding.bodyTextView.layoutParams
bodyTextView.getIntersectedModalSpans(event).forEach { span -> params.width = if (onlyBodyMessage) ViewGroup.LayoutParams.WRAP_CONTENT else 0
span.onClick(bodyTextView) binding.bodyTextView.layoutParams = params
if (message.body.isNotEmpty() && !hideBody) {
val color = getTextColor(context, message)
binding.bodyTextView.setTextColor(color)
binding.bodyTextView.setLinkTextColor(color)
val body = getBodySpans(context, message, searchQuery)
binding.bodyTextView.text = body
onContentClick.add { e: MotionEvent ->
binding.bodyTextView.getIntersectedModalSpans(e).forEach { span ->
span.onClick(binding.bodyTextView)
} }
} }
} }
@ -207,35 +229,27 @@ class VisibleMessageContentView : LinearLayout {
} }
fun recycle() { fun recycle() {
binding.mainContainer.removeAllViews() arrayOf(
binding.deletedMessageView,
binding.untrustedView,
binding.voiceMessageView,
binding.openGroupInvitationView,
binding.documentView,
binding.quoteView,
binding.linkPreviewView,
binding.albumThumbnailView,
binding.bodyTextView
).forEach { view -> view.isVisible = false }
} }
fun playVoiceMessage() { fun playVoiceMessage() {
binding.mainContainer.children.forEach { view -> binding.voiceMessageView.togglePlayback()
if (view is VoiceMessageView) {
return@forEach view.togglePlayback()
}
}
} }
// endregion // endregion
// region Convenience // region Convenience
companion object { companion object {
fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView {
val result = EmojiTextView(context)
val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt()
val hPadding = toPx(12, context.resources)
result.setPadding(hPadding, vPadding, hPadding, vPadding)
result.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.resources.getDimension(R.dimen.small_font_size))
val color = getTextColor(context, message)
result.setTextColor(color)
result.setLinkTextColor(color)
val body = getBodySpans(context, message, searchQuery)
result.text = body
return result
}
fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable { fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable {
var body = message.body.toSpannable() var body = message.body.toSpannable()

View File

@ -15,7 +15,6 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
@ -181,7 +180,7 @@ class VisibleMessageView : LinearLayout {
if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width } if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width }
// Populate content view // Populate content view
binding.messageContentView.indexInAdapter = indexInAdapter binding.messageContentView.indexInAdapter = indexInAdapter
binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false)) binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false))
binding.messageContentView.delegate = contentViewDelegate binding.messageContentView.delegate = contentViewDelegate
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() } onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
} }
@ -224,11 +223,13 @@ class VisibleMessageView : LinearLayout {
} }
private fun updateExpirationTimer(message: MessageRecord) { private fun updateExpirationTimer(message: MessageRecord) {
val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as RelativeLayout.LayoutParams val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as MarginLayoutParams
val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END val container = binding.expirationTimerViewContainer
val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START val content = binding.messageContentView
expirationTimerViewLayoutParams.removeRule(ruleToRemove) val expiration = binding.expirationTimerView
expirationTimerViewLayoutParams.addRule(ruleToAdd, R.id.messageContentView) container.removeAllViewsInLayout()
container.addView(if (message.isOutgoing) expiration else content)
container.addView(if (message.isOutgoing) content else expiration)
val expirationTimerViewSize = toPx(12, resources) val expirationTimerViewSize = toPx(12, resources)
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt() val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0 expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
@ -261,6 +262,7 @@ class VisibleMessageView : LinearLayout {
} else { } else {
binding.expirationTimerView.isVisible = false binding.expirationTimerView.isVisible = false
} }
container.requestLayout()
} }
private fun handleIsSelectedChanged() { private fun handleIsSelectedChanged() {
@ -388,7 +390,7 @@ class VisibleMessageView : LinearLayout {
} }
fun onContentClick(event: MotionEvent) { fun onContentClick(event: MotionEvent) {
binding.messageContentView.onContentClick?.invoke(event) binding.messageContentView.onContentClick.forEach { clickHandler -> clickHandler.invoke(event) }
} }
private fun onPress(event: MotionEvent) { private fun onPress(event: MotionEvent) {

View File

@ -1,19 +1,19 @@
package org.thoughtcrime.securesms.conversation.v2.utilities; package org.thoughtcrime.securesms.conversation.v2.utilities;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import android.util.AttributeSet; import android.util.AttributeSet;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsignal.utilities.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
@ -22,8 +22,13 @@ import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import network.loki.messenger.R; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.SettableFuture;
import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget; import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget;
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget; import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
import org.thoughtcrime.securesms.components.TransferControlView; import org.thoughtcrime.securesms.components.TransferControlView;
@ -33,17 +38,11 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsignal.utilities.guava.Optional;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.SettableFuture;
import java.util.Collections; import java.util.Collections;
import java.util.Locale; import java.util.Locale;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; import network.loki.messenger.R;
public class ThumbnailView extends FrameLayout { public class ThumbnailView extends FrameLayout {
@ -287,7 +286,7 @@ public class ThumbnailView extends FrameLayout {
} else if (slide.hasPlaceholder()) { } else if (slide.hasPlaceholder()) {
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result)); buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result));
} else { } else {
glideRequests.clear(image); glideRequests.load(R.drawable.ic_image_white_24dp).centerInside().into(image);
result.set(false); result.set(false);
} }

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -22,58 +20,4 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout="@layout/transfer_controls_stub" /> android:layout="@layout/transfer_controls_stub" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_alignTop="@+id/albumCellContainer"
android:layout_alignStart="@+id/albumCellContainer"
android:layout_alignEnd="@+id/albumCellContainer"
android:layout_alignBottom="@+id/albumCellContainer"
tools:visibility="visible"
android:visibility="gone"
android:layout_gravity="bottom"
android:id="@+id/albumCellBodyParent"
android:layout_width="0dp"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/albumCellShade"
android:src="@drawable/image_shade"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@+id/albumCellBodyTextParent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<LinearLayout
android:id="@+id/albumCellBodyTextParent"
android:padding="@dimen/medium_spacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/albumCellBodyTextReadMore"
android:orientation="horizontal">
<View
android:layout_width="@dimen/accent_line_thickness"
android:layout_height="match_parent"
android:background="@color/accent"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:maxLines="4"
android:ellipsize="end"
android:id="@+id/albumCellBodyText"
android:textColor="@color/core_white"
android:layout_marginStart="@dimen/small_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
tools:visibility="visible"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:id="@+id/albumCellBodyTextReadMore"
android:textColor="@color/core_white"
android:paddingHorizontal="@dimen/medium_spacing"
android:paddingBottom="@dimen/medium_spacing"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ConversationItem_read_more"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -41,7 +41,8 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:textSize="@dimen/small_font_size" android:textSize="@dimen/small_font_size"
android:textStyle="bold" android:textStyle="bold"
tools:text="The Day The Dinosaurs Died - Minute by Minute" tools:text="Some Text here"
android:minWidth="@dimen/media_bubble_min_width"
android:maxLines="3" android:maxLines="3"
android:ellipsize="end" android:ellipsize="end"
android:textColor="@color/text" android:textColor="@color/text"

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <RelativeLayout android:id="@+id/mainQuoteViewContainer"
android:id="@+id/mainQuoteViewContainer"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/input_bar_background" android:background="@color/input_bar_background"
android:paddingHorizontal="@dimen/medium_spacing"> android:paddingHorizontal="@dimen/medium_spacing"
xmlns:tools="http://schemas.android.com/tools">
<View <View
android:id="@+id/quoteViewAccentLine" android:id="@+id/quoteViewAccentLine"
@ -21,6 +21,7 @@
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_marginVertical="@dimen/small_spacing"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:background="@drawable/view_quote_attachment_preview_background"> android:background="@drawable/view_quote_attachment_preview_background">
@ -45,23 +46,25 @@
android:id="@+id/quoteViewMainContentContainer" android:id="@+id/quoteViewMainContentContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="30dp"
android:gravity="center_vertical"
android:layout_alignParentStart="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginVertical="@dimen/small_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_marginEnd="@dimen/medium_spacing"
android:layout_toEndOf="@+id/quoteViewAttachmentPreviewContainer"
android:layout_toStartOf="@+id/quoteViewCancelButton"
android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/quoteViewAuthorTextView" android:id="@+id/quoteViewAuthorTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Spiderman" android:ellipsize="end"
android:textSize="@dimen/small_font_size"
android:textStyle="bold"
android:textColor="@color/text"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" /> tools:text="Spiderman"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
android:textStyle="bold" />
<TextView <TextView
android:id="@+id/quoteViewBodyTextView" android:id="@+id/quoteViewBodyTextView"
@ -69,7 +72,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="3" android:maxLines="3"
android:text="Yo, I need your help here!" tools:text="Yo, I need your help here!"
android:textColor="@color/text" android:textColor="@color/text"
android:textSize="@dimen/small_font_size" /> android:textSize="@dimen/small_font_size" />

View File

@ -75,8 +75,9 @@
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" /> android:ellipsize="end" />
<RelativeLayout <LinearLayout
android:id="@+id/expirationTimerViewContainer" android:id="@+id/expirationTimerViewContainer"
android:orientation="horizontal"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -89,9 +90,9 @@
android:id="@+id/expirationTimerView" android:id="@+id/expirationTimerView"
android:layout_width="12dp" android:layout_width="12dp"
android:layout_height="12dp" android:layout_height="12dp"
android:layout_centerVertical="true" /> android:layout_gravity="center_vertical" />
</RelativeLayout> </LinearLayout>
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -100,12 +101,12 @@
<TextView <TextView
android:id="@+id/messageTimestampTextView" android:id="@+id/messageTimestampTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:maxLines="1" android:maxLines="1"
android:textSize="10dp" /> android:textSize="11sp" />
<ImageView <ImageView
android:id="@+id/messageStatusImageView" android:id="@+id/messageStatusImageView"

View File

@ -1,7 +1,104 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" /> xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mainContainerConstraint"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Content that will only show on its own -->
<org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:id="@+id/deletedMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.conversation.v2.messages.UntrustedAttachmentView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:id="@+id/untrustedView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.conversation.v2.messages.VoiceMessageView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:id="@+id/voiceMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:id="@+id/openGroupInvitationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.conversation.v2.messages.DocumentView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:visibility="gone"
android:id="@+id/documentView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!-- Content that will show with other elements -->
<org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@+id/albumThumbnailView"
app:layout_constraintHorizontal_bias="0"
tools:visibility="visible"
android:visibility="gone"
android:id="@+id/quoteView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView
app:layout_constraintTop_toBottomOf="@+id/quoteView"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible"
android:visibility="gone"
android:id="@+id/linkPreviewView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bodyBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="albumThumbnailView,linkPreviewView,quoteView,voiceMessageView"/>
<org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
app:layout_constraintTop_toBottomOf="@+id/linkPreviewView"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:id="@+id/albumThumbnailView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
app:layout_constraintHorizontal_bias="0"
tools:visibility="visible"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/albumThumbnailView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@+id/bodyBarrier"
android:paddingHorizontal="12dp"
android:paddingVertical="@dimen/small_spacing"
android:id="@+id/bodyTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>