mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Highlight @You mentions (#985)
* Highlight @You mentions * fix: resolve merge conflicts * Setting the proper design rules for mentions * New RoundedBackgroundSpan, applied to "you" mentions The rounded background highlighter can take padding, so there is no need to add those extra spaces at the start and end. * Better mention highlight logic Some mention highlight should only format the text and not apply any styling. Also making sure we cater for all cases properly * Updated the text color logic based on design rules * Fine tuning the color rules * Removing usage of Resources.getSystem() Only making the db call if there actually is a mention * Moving color definition outside the loop to avoid repetitions --------- Co-authored-by: charles <charles@oxen.io> Co-authored-by: 0x330a <92654767+0x330a@users.noreply.github.com> Co-authored-by: ThomasSession <thomas.r@getsession.org>
This commit is contained in:
parent
1d80bb0ba9
commit
a260717d42
@ -1958,7 +1958,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val messageIterator = sortedMessages.iterator()
|
||||
while (messageIterator.hasNext()) {
|
||||
val message = messageIterator.next()
|
||||
val body = MentionUtilities.highlightMentions(message.body, viewModel.threadId, this)
|
||||
val body = MentionUtilities.highlightMentions(
|
||||
text = message.body,
|
||||
formatOnly = true, // no styling here, only text formatting
|
||||
threadID = viewModel.threadId,
|
||||
context = this
|
||||
)
|
||||
|
||||
if (TextUtils.isEmpty(body)) { continue }
|
||||
if (messageSize > 1) {
|
||||
val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp)
|
||||
|
@ -80,7 +80,15 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
binding.quoteViewAuthorTextView.text = authorDisplayName
|
||||
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
// Body
|
||||
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context)
|
||||
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation)
|
||||
resources.getString(R.string.open_group_invitation_view__open_group_invitation)
|
||||
else MentionUtilities.highlightMentions(
|
||||
text = (body ?: "").toSpannable(),
|
||||
isOutgoingMessage = isOutgoingMessage,
|
||||
isQuote = true,
|
||||
threadID = threadID,
|
||||
context = context
|
||||
)
|
||||
binding.quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
// Accent line / attachment preview
|
||||
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing
|
||||
|
@ -282,7 +282,12 @@ class VisibleMessageContentView : ConstraintLayout {
|
||||
fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable {
|
||||
var body = message.body.toSpannable()
|
||||
|
||||
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
|
||||
body = MentionUtilities.highlightMentions(
|
||||
text = body,
|
||||
isOutgoingMessage = message.isOutgoing,
|
||||
threadID = message.threadId,
|
||||
context = context
|
||||
)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||
{ BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
@ -9,43 +9,60 @@ import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Range
|
||||
import androidx.appcompat.widget.ThemeUtils
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.combine.Tuple2
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.RoundedBackgroundSpan
|
||||
import org.thoughtcrime.securesms.util.getAccentColor
|
||||
import org.thoughtcrime.securesms.util.getColorResourceIdFromAttr
|
||||
import org.thoughtcrime.securesms.util.getMessageTextColourAttr
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object MentionUtilities {
|
||||
|
||||
@JvmStatic
|
||||
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
|
||||
return highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
|
||||
}
|
||||
private val pattern by lazy { Pattern.compile("@[0-9a-fA-F]*") }
|
||||
|
||||
/**
|
||||
* Highlights mentions in a given text.
|
||||
*
|
||||
* @param text The text to highlight mentions in.
|
||||
* @param isOutgoingMessage Whether the message is outgoing.
|
||||
* @param isQuote Whether the message is a quote.
|
||||
* @param formatOnly Whether to only format the mentions. If true we only format the text itself,
|
||||
* for example resolving an accountID to a username. If false we also apply styling, like colors and background.
|
||||
* @param threadID The ID of the thread the message belongs to.
|
||||
* @param context The context to use.
|
||||
* @return A SpannableString with highlighted mentions.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
|
||||
fun highlightMentions(
|
||||
text: CharSequence,
|
||||
isOutgoingMessage: Boolean = false,
|
||||
isQuote: Boolean = false,
|
||||
formatOnly: Boolean = false,
|
||||
threadID: Long,
|
||||
context: Context
|
||||
): SpannableString {
|
||||
@Suppress("NAME_SHADOWING") var text = text
|
||||
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
||||
|
||||
var matcher = pattern.matcher(text)
|
||||
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
||||
var startIndex = 0
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val openGroup = DatabaseComponent.get(context).storage().getOpenGroup(threadID)
|
||||
val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) }
|
||||
|
||||
// format the mention text
|
||||
if (matcher.find(startIndex)) {
|
||||
while (true) {
|
||||
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
||||
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.sessionId(userPublicKey, publicKey, it.publicKey) } ?: false
|
||||
val userDisplayName: String? = if (publicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey) {
|
||||
val isYou = isYou(publicKey, userPublicKey, openGroup)
|
||||
val userDisplayName: String? = if (isYou) {
|
||||
context.getString(R.string.MessageRecord_you)
|
||||
} else {
|
||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||
@ -53,7 +70,8 @@ object MentionUtilities {
|
||||
contact?.displayName(context)
|
||||
}
|
||||
if (userDisplayName != null) {
|
||||
text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length)
|
||||
val mention = "@$userDisplayName"
|
||||
text = text.subSequence(0, matcher.start()).toString() + mention + text.subSequence(matcher.end(), text.length)
|
||||
val endIndex = matcher.start() + 1 + userDisplayName.length
|
||||
startIndex = endIndex
|
||||
mentions.add(Tuple2(Range.create(matcher.start(), endIndex), publicKey))
|
||||
@ -66,37 +84,83 @@ object MentionUtilities {
|
||||
}
|
||||
val result = SpannableString(text)
|
||||
|
||||
var mentionTextColour: Int? = null
|
||||
// In dark themes..
|
||||
if (ThemeUtil.isDarkTheme(context)) {
|
||||
// ..we use the standard outgoing message colour for outgoing messages..
|
||||
if (isOutgoingMessage) {
|
||||
val mentionTextColourAttributeId = getMessageTextColourAttr(true)
|
||||
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
|
||||
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
|
||||
}
|
||||
else // ..but we use the accent colour for incoming messages (i.e., someone mentioning us)..
|
||||
{
|
||||
mentionTextColour = context.getAccentColor()
|
||||
}
|
||||
}
|
||||
else // ..while in light themes we always just use the incoming or outgoing message text colour for mentions.
|
||||
{
|
||||
val mentionTextColourAttributeId = getMessageTextColourAttr(isOutgoingMessage)
|
||||
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
|
||||
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
|
||||
// apply styling if required
|
||||
// Normal text color: black in dark mode and primary text color for light mode
|
||||
val mainTextColor by lazy {
|
||||
if (ThemeUtil.isDarkTheme(context)) context.getColor(R.color.black)
|
||||
else context.getColorFromAttr(android.R.attr.textColorPrimary)
|
||||
}
|
||||
|
||||
for (mention in mentions) {
|
||||
result.setSpan(ForegroundColorSpan(mentionTextColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
// Highlighted text color: primary/accent in dark mode and primary text color for light mode
|
||||
val highlightedTextColor by lazy {
|
||||
if (ThemeUtil.isDarkTheme(context)) context.getAccentColor()
|
||||
else context.getColorFromAttr(android.R.attr.textColorPrimary)
|
||||
}
|
||||
|
||||
// If we're using a light theme then we change the background colour of the mention to be the accent colour
|
||||
if (ThemeUtil.isLightTheme(context)) {
|
||||
val backgroundColour = context.getAccentColor();
|
||||
result.setSpan(BackgroundColorSpan(backgroundColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
if(!formatOnly) {
|
||||
for (mention in mentions) {
|
||||
val backgroundColor: Int?
|
||||
val foregroundColor: Int?
|
||||
|
||||
// quotes
|
||||
if(isQuote) {
|
||||
backgroundColor = null
|
||||
// the text color has different rule depending if the message is incoming or outgoing
|
||||
foregroundColor = if(isOutgoingMessage) null else highlightedTextColor
|
||||
}
|
||||
// incoming message mentioning you
|
||||
else if (isYou(mention.second, userPublicKey, openGroup)) {
|
||||
backgroundColor = context.getAccentColor()
|
||||
foregroundColor = mainTextColor
|
||||
}
|
||||
// outgoing message
|
||||
else if (isOutgoingMessage) {
|
||||
backgroundColor = null
|
||||
foregroundColor = mainTextColor
|
||||
}
|
||||
// incoming messages mentioning someone else
|
||||
else {
|
||||
backgroundColor = null
|
||||
// accent color for dark themes and primary text for light
|
||||
foregroundColor = highlightedTextColor
|
||||
}
|
||||
|
||||
// apply the background, if any
|
||||
backgroundColor?.let { background ->
|
||||
result.setSpan(
|
||||
RoundedBackgroundSpan(
|
||||
context = context,
|
||||
textColor = mainTextColor,
|
||||
backgroundColor = background
|
||||
),
|
||||
mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// apply the foreground, if any
|
||||
foregroundColor?.let {
|
||||
result.setSpan(
|
||||
ForegroundColorSpan(it),
|
||||
mention.first.lower,
|
||||
mention.first.upper,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// apply bold on the mention
|
||||
result.setSpan(
|
||||
StyleSpan(Typeface.BOLD),
|
||||
mention.first.lower,
|
||||
mention.first.upper,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun isYou(mentionedPublicKey: String, userPublicKey: String, openGroup: OpenGroup?): Boolean {
|
||||
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.sessionId(userPublicKey, mentionedPublicKey, it.publicKey) } ?: false
|
||||
return mentionedPublicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey
|
||||
}
|
||||
}
|
@ -103,7 +103,12 @@ class ConversationView : LinearLayout {
|
||||
R.drawable.ic_notifications_mentions
|
||||
}
|
||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||
binding.snippetTextView.text = highlightMentions(thread.getSnippet(), thread.threadId, context)
|
||||
binding.snippetTextView.text = highlightMentions(
|
||||
text = thread.getSnippet(),
|
||||
formatOnly = true, // no styling here, only text formatting
|
||||
threadID = thread.threadId,
|
||||
context = context
|
||||
)
|
||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
if (isTyping) {
|
||||
|
@ -39,7 +39,13 @@ class MessageRequestView : LinearLayout {
|
||||
binding.displayNameTextView.text = senderDisplayName
|
||||
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
val rawSnippet = thread.getDisplayBody(context)
|
||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
||||
val snippet = highlightMentions(
|
||||
text = rawSnippet,
|
||||
formatOnly = true, // no styling here, only text formatting
|
||||
threadID = thread.threadId,
|
||||
context = context
|
||||
)
|
||||
|
||||
binding.snippetTextView.text = snippet
|
||||
|
||||
post {
|
||||
|
@ -444,13 +444,30 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
while(iterator.hasPrevious()) {
|
||||
NotificationItem item = iterator.previous();
|
||||
builder.addMessageBody(item.getIndividualRecipient(), item.getRecipient(),
|
||||
MentionUtilities.highlightMentions(item.getText(), item.getThreadId(), context));
|
||||
MentionUtilities.highlightMentions(
|
||||
item.getText() != null ? item.getText() : "",
|
||||
false,
|
||||
false,
|
||||
true, // no styling here, only text formatting
|
||||
item.getThreadId(),
|
||||
context
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (signal) {
|
||||
builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate());
|
||||
CharSequence text = notifications.get(0).getText();
|
||||
builder.setTicker(notifications.get(0).getIndividualRecipient(),
|
||||
MentionUtilities.highlightMentions(notifications.get(0).getText(), notifications.get(0).getThreadId(), context));
|
||||
MentionUtilities.highlightMentions(
|
||||
text != null ? text : "",
|
||||
false,
|
||||
false,
|
||||
true, // no styling here, only text formatting
|
||||
notifications.get(0).getThreadId(),
|
||||
context
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag);
|
||||
|
@ -0,0 +1,61 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.text.style.ReplacementSpan
|
||||
|
||||
/**
|
||||
* A Span that draws text with a rounded background.
|
||||
*
|
||||
* @param textColor - The color of the text.
|
||||
* @param backgroundColor - The color of the background.
|
||||
* @param cornerRadius - The corner radius of the background in pixels. Defaults to 8dp.
|
||||
* @param paddingHorizontal - The horizontal padding of the text in pixels. Defaults to 3dp.
|
||||
* @param paddingVertical - The vertical padding of the text in pixels. Defaults to 3dp.
|
||||
*/
|
||||
|
||||
|
||||
class RoundedBackgroundSpan(
|
||||
context: Context,
|
||||
private val textColor: Int,
|
||||
private val backgroundColor: Int,
|
||||
private val cornerRadius: Float = toPx(8, context.resources).toFloat(), // setting some Session defaults
|
||||
private val paddingHorizontal: Float = toPx(3, context.resources).toFloat(),
|
||||
private val paddingVertical: Float = toPx(3, context.resources).toFloat()
|
||||
) : ReplacementSpan() {
|
||||
|
||||
override fun draw(
|
||||
canvas: Canvas, text: CharSequence, start: Int, end: Int,
|
||||
x: Float, top: Int, y: Int, bottom: Int, paint: Paint
|
||||
) {
|
||||
// the top needs to take into account the font and the required vertical padding
|
||||
val newTop = y + paint.fontMetrics.ascent - paddingVertical
|
||||
val newBottom = y + paint.fontMetrics.descent + paddingVertical
|
||||
val rect = RectF(
|
||||
x,
|
||||
newTop,
|
||||
x + measureText(paint, text, start, end) + 2 * paddingHorizontal,
|
||||
newBottom
|
||||
)
|
||||
paint.color = backgroundColor
|
||||
|
||||
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
|
||||
paint.color = textColor
|
||||
canvas.drawText(text, start, end, x + paddingHorizontal, y.toFloat(), paint)
|
||||
}
|
||||
|
||||
override fun getSize(
|
||||
paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?
|
||||
): Int {
|
||||
return (paint.measureText(text, start, end) + 2 * paddingHorizontal).toInt()
|
||||
}
|
||||
|
||||
private fun measureText(
|
||||
paint: Paint, text: CharSequence, start: Int, end: Int
|
||||
): Float {
|
||||
return paint.measureText(text, start, end)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user