mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-26 22:20:20 +00:00
CFV2 Add proper body presentation code.
This commit is contained in:
@@ -100,6 +100,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.conversation.ui.payment.PaymentMessageView;
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement;
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationBodyUtil;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.database.MediaTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
@@ -1509,7 +1510,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private void linkifyMessageBody(@NonNull Spannable messageBody,
|
||||
boolean shouldLinkifyAllLinks)
|
||||
{
|
||||
linkifyUrlLinks(messageBody, shouldLinkifyAllLinks, urlClickListener);
|
||||
V2ConversationBodyUtil.linkifyUrlLinks(messageBody, shouldLinkifyAllLinks, urlClickListener);
|
||||
|
||||
if (conversationMessage.hasStyleLinks()) {
|
||||
for (PlaceholderURLSpan placeholder : messageBody.getSpans(0, messageBody.length(), PlaceholderURLSpan.class)) {
|
||||
@@ -1529,29 +1530,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static void linkifyUrlLinks(@NonNull Spannable messageBody, boolean shouldLinkifyAllLinks, @NonNull UrlClickHandler urlClickHandler) {
|
||||
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
||||
boolean hasLinks = LinkifyCompat.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
|
||||
|
||||
if (hasLinks) {
|
||||
Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class))
|
||||
.filterNot(url -> LinkUtil.isLegalUrl(url.getURL()))
|
||||
.forEach(messageBody::removeSpan);
|
||||
|
||||
URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class);
|
||||
|
||||
for (URLSpan urlSpan : urlSpans) {
|
||||
int start = messageBody.getSpanStart(urlSpan);
|
||||
int end = messageBody.getSpanEnd(urlSpan);
|
||||
URLSpan span = new InterceptableLongClickCopyLinkSpan(urlSpan.getURL(), urlClickHandler);
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
messageBody.removeSpan(urlSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStatusIcons(MessageRecord messageRecord, boolean hasWallpaper) {
|
||||
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class ConversationAdapterV2(
|
||||
override val selectedItems: Set<MultiselectPart>
|
||||
get() = _selected.toSet()
|
||||
|
||||
private var searchQuery: String? = null
|
||||
override var searchQuery: String? = null
|
||||
private var inlineContent: ConversationMessage? = null
|
||||
|
||||
private var recordToPulse: ConversationMessage? = null
|
||||
@@ -80,6 +80,9 @@ class ConversationAdapterV2(
|
||||
|
||||
private val condensedMode: ConversationItemDisplayMode? = null
|
||||
|
||||
// TODO [cfv2]
|
||||
override val isMessageRequestAccepted: Boolean = true
|
||||
|
||||
init {
|
||||
registerFactory(ThreadHeader::class.java, ::ThreadHeaderViewHolder, R.layout.conversation_item_thread_header)
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan
|
||||
import org.thoughtcrime.securesms.util.LinkUtil
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler
|
||||
|
||||
/**
|
||||
* Utilities for presenting the body of a conversation message.
|
||||
*/
|
||||
object V2ConversationBodyUtil {
|
||||
|
||||
@JvmStatic
|
||||
fun linkifyUrlLinks(messageBody: Spannable, shouldLinkifyAllLinks: Boolean, urlClickHandler: UrlClickHandler) {
|
||||
val linkPattern = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS
|
||||
val hasLinks = LinkifyCompat.addLinks(messageBody, if (shouldLinkifyAllLinks) linkPattern else 0)
|
||||
|
||||
if (!hasLinks) {
|
||||
return
|
||||
}
|
||||
|
||||
messageBody.getSpans(0, messageBody.length, URLSpan::class.java)
|
||||
.filterNot { LinkUtil.isLegalUrl(it.url) }
|
||||
.forEach(messageBody::removeSpan)
|
||||
|
||||
messageBody.getSpans(0, messageBody.length, URLSpan::class.java).forEach { urlSpan ->
|
||||
val start = messageBody.getSpanStart(urlSpan)
|
||||
val end = messageBody.getSpanEnd(urlSpan)
|
||||
val span = InterceptableLongClickCopyLinkSpan(urlSpan.url, urlClickHandler)
|
||||
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
messageBody.removeSpan(urlSpan)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
/**
|
||||
@@ -17,6 +18,9 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
interface V2ConversationContext {
|
||||
val displayMode: ConversationItemDisplayMode
|
||||
val clickListener: ConversationAdapter.ItemClickListener
|
||||
val selectedItems: Set<MultiselectPart>
|
||||
val isMessageRequestAccepted: Boolean
|
||||
val searchQuery: String?
|
||||
|
||||
fun onStartExpirationTimeout(messageRecord: MessageRecord)
|
||||
|
||||
|
||||
@@ -5,15 +5,30 @@
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect
|
||||
@@ -23,12 +38,22 @@ import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElemen
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod
|
||||
import org.thoughtcrime.securesms.util.PlaceholderURLSpan
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.ProjectionList
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.hasExtraText
|
||||
import org.thoughtcrime.securesms.util.hasNoBubble
|
||||
import org.thoughtcrime.securesms.util.isScheduled
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
@@ -53,6 +78,11 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
private val conversationContext: V2ConversationContext
|
||||
) : V2BaseViewHolder<Model>(binding.root, conversationContext), Multiselectable, InteractiveConversationElement {
|
||||
|
||||
companion object {
|
||||
private val STYLE_FACTORY = StyleFactory { arrayOf<CharacterStyle>(BackgroundColorSpan(Color.YELLOW), ForegroundColorSpan(Color.BLACK)) }
|
||||
private const val CONDENSED_MODE_MAX_LINES = 3
|
||||
}
|
||||
|
||||
private var messageId: Long = Long.MAX_VALUE
|
||||
|
||||
private val projections = ProjectionList()
|
||||
@@ -107,6 +137,17 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
binding.conversationItemBody.isClickable = false
|
||||
binding.conversationItemBody.isFocusable = false
|
||||
binding.conversationItemBody.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().messageFontSize.toFloat())
|
||||
binding.conversationItemBody.movementMethod = LongClickMovementMethod.getInstance(context)
|
||||
|
||||
if (binding.isIncoming) {
|
||||
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, if (ThemeUtil.isDarkTheme(context)) R.color.core_grey_60 else R.color.core_grey_20))
|
||||
} else {
|
||||
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25))
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
@@ -120,13 +161,12 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
adapterPosition = bindingAdapterPosition
|
||||
)
|
||||
|
||||
binding.conversationItemBody.setTextColor(themeDelegate.getBodyTextColor(conversationMessage))
|
||||
shapeDelegate.bodyBubble.fillColor = themeDelegate.getBodyBubbleColor(conversationMessage)
|
||||
|
||||
binding.conversationItemBody.text = conversationMessage.getDisplayBody(context)
|
||||
binding.conversationItemBodyWrapper.background = shapeDelegate.bodyBubble
|
||||
binding.conversationItemReply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor())
|
||||
|
||||
presentBody()
|
||||
presentDate(shape)
|
||||
presentDeliveryStatus(shape)
|
||||
presentFooterBackground(shape)
|
||||
@@ -234,6 +274,99 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||
return if (isMms) -id else id
|
||||
}
|
||||
|
||||
private fun presentBody() {
|
||||
binding.conversationItemBody.setTextColor(themeDelegate.getBodyTextColor(conversationMessage))
|
||||
binding.conversationItemBody.setLinkTextColor(themeDelegate.getBodyTextColor(conversationMessage))
|
||||
|
||||
val record = conversationMessage.messageRecord
|
||||
var styledText: Spannable = conversationMessage.getDisplayBody(context)
|
||||
if (conversationContext.isMessageRequestAccepted) {
|
||||
linkifyMessageBody(styledText)
|
||||
}
|
||||
|
||||
styledText = SearchUtil.getHighlightedSpan(Locale.getDefault(), STYLE_FACTORY, styledText, conversationContext.searchQuery, SearchUtil.STRICT)
|
||||
if (record.hasExtraText()) {
|
||||
binding.conversationItemBody.setOverflowText(getLongMessageSpan())
|
||||
} else {
|
||||
binding.conversationItemBody.setOverflowText(null)
|
||||
}
|
||||
|
||||
if (isContentCondensed()) {
|
||||
binding.conversationItemBody.maxLines = CONDENSED_MODE_MAX_LINES
|
||||
} else {
|
||||
binding.conversationItemBody.maxLines = Integer.MAX_VALUE
|
||||
}
|
||||
|
||||
binding.conversationItemBody.text = StringUtil.trim(styledText)
|
||||
}
|
||||
|
||||
private fun linkifyMessageBody(messageBody: Spannable) {
|
||||
V2ConversationBodyUtil.linkifyUrlLinks(messageBody, conversationContext.selectedItems.isEmpty(), conversationContext.clickListener::onUrlClicked)
|
||||
|
||||
if (conversationMessage.hasStyleLinks()) {
|
||||
messageBody.getSpans(0, messageBody.length, PlaceholderURLSpan::class.java).forEach { placeholder ->
|
||||
val start = messageBody.getSpanStart(placeholder)
|
||||
val end = messageBody.getSpanEnd(placeholder)
|
||||
val span: URLSpan = InterceptableLongClickCopyLinkSpan(
|
||||
placeholder.value,
|
||||
conversationContext.clickListener::onUrlClicked,
|
||||
ContextCompat.getColor(getContext(), R.color.signal_accent_primary),
|
||||
false
|
||||
)
|
||||
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
MentionAnnotation.getMentionAnnotations(messageBody).forEach { annotation ->
|
||||
messageBody.setSpan(
|
||||
MentionClickableSpan(RecipientId.from(annotation.value)),
|
||||
messageBody.getSpanStart(annotation),
|
||||
messageBody.getSpanEnd(annotation),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLongMessageSpan(): CharSequence {
|
||||
val message = context.getString(R.string.ConversationItem_read_more)
|
||||
val span = SpannableStringBuilder(message)
|
||||
|
||||
span.setSpan(ReadMoreSpan(), 0, span.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
return span
|
||||
}
|
||||
|
||||
private inner class MentionClickableSpan(
|
||||
private val recipientId: RecipientId
|
||||
) : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
VibrateUtil.vibrateTick(context)
|
||||
conversationContext.clickListener.onGroupMemberClicked(recipientId, conversationMessage.threadRecipient.requireGroupId())
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) = Unit
|
||||
}
|
||||
|
||||
private inner class ReadMoreSpan : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
if (conversationContext.selectedItems.isEmpty()) {
|
||||
conversationContext.clickListener.onMoreTextClicked(
|
||||
conversationMessage.threadRecipient.id,
|
||||
conversationMessage.messageRecord.id,
|
||||
conversationMessage.messageRecord.isMms
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
ds.typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
}
|
||||
|
||||
private fun isContentCondensed(): Boolean {
|
||||
return conversationContext.displayMode == ConversationItemDisplayMode.CONDENSED && conversationContext.getPreviousMessage(bindingAdapterPosition) == null
|
||||
}
|
||||
|
||||
private fun presentFooterExpiry(shape: V2ConversationItemShape.MessageShape) {
|
||||
if (shape == V2ConversationItemShape.MessageShape.MIDDLE || shape == V2ConversationItemShape.MessageShape.START) {
|
||||
binding.conversationItemFooterExpiry.stopAnimation()
|
||||
|
||||
Reference in New Issue
Block a user