Fix member label emoji ignoring use system emoji preference.

This commit is contained in:
jeffrey-signal
2026-02-20 09:41:04 -05:00
committed by Cody Henthorne
parent fa2b0aedb0
commit 7e605fb6de
5 changed files with 88 additions and 45 deletions

View File

@@ -5,11 +5,14 @@
package org.thoughtcrime.securesms.components.emoji
import android.content.Context
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -21,10 +24,12 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.TextUnit
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* Applies Signal or System emoji to the given content based off user settings.
@@ -34,6 +39,7 @@ import org.signal.core.ui.compose.Previews
@Composable
fun Emojifier(
text: String,
useSystemEmoji: Boolean = !LocalInspectionMode.current && SignalStore.settings.isPreferSystemEmoji,
content: @Composable (AnnotatedString, Map<String, InlineTextContent>) -> Unit = { annotatedText, inlineContent ->
Text(
text = annotatedText,
@@ -41,38 +47,56 @@ fun Emojifier(
)
}
) {
if (LocalInspectionMode.current) {
if (useSystemEmoji) {
content(buildAnnotatedString { append(text) }, emptyMap())
return
}
val context = LocalContext.current
val candidates = remember(text) { EmojiProvider.getCandidates(text) }
val candidateMap: Map<String, InlineTextContent> = remember(text) {
candidates?.associate { candidate ->
candidate.drawInfo.emoji to InlineTextContent(placeholder = Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)) {
Image(
painter = rememberDrawablePainter(EmojiProvider.getEmojiDrawable(context, candidate.drawInfo.emoji)),
contentDescription = null
)
}
} ?: emptyMap()
val fontSize = LocalTextStyle.current.fontSize
val foundEmojis: List<EmojiParser.Candidate> = remember(text) {
EmojiProvider.getCandidates(text)?.list.orEmpty()
}
val inlineContentByEmoji: Map<String, InlineTextContent> = remember(text, fontSize) {
foundEmojis.associate { it.drawInfo.emoji to createInlineContent(context, it.drawInfo.emoji, fontSize) }
}
val annotatedString = buildAnnotatedString {
append(text)
val annotatedString = remember(text) { buildAnnotatedString(text, foundEmojis) }
content(annotatedString, inlineContentByEmoji)
}
candidates?.forEach {
addStringAnnotation(
tag = "EMOJI",
annotation = it.drawInfo.emoji,
start = it.startIndex,
end = it.endIndex
)
private fun createInlineContent(context: Context, emoji: String, fontSize: TextUnit): InlineTextContent {
return InlineTextContent(
placeholder = Placeholder(width = fontSize, height = fontSize, PlaceholderVerticalAlign.TextCenter)
) {
Image(
painter = rememberDrawablePainter(EmojiProvider.getEmojiDrawable(context, emoji)),
contentDescription = null
)
}
}
/**
* Constructs an [AnnotatedString] from [text], substituting each emoji in [foundEmojis] with an inline content placeholder.
*/
private fun buildAnnotatedString(
text: String,
foundEmojis: List<EmojiParser.Candidate>
): AnnotatedString = buildAnnotatedString {
var nextSegmentStartIndex = 0
foundEmojis.forEach { emoji ->
if (emoji.startIndex > nextSegmentStartIndex) {
append(text, start = nextSegmentStartIndex, end = emoji.startIndex)
}
appendInlineContent(emoji.drawInfo.emoji)
nextSegmentStartIndex = emoji.endIndex
}
content(annotatedString, candidateMap)
if (nextSegmentStartIndex < text.length) {
append(text, start = nextSegmentStartIndex, end = text.length)
}
}
@Composable

View File

@@ -41,6 +41,9 @@ public class EmojiParser {
this.emojiTree = emojiTree;
}
/**
* Returns an ordered list of every emoji occurrence found in the given text.
*/
public @NonNull CandidateList findCandidates(@Nullable CharSequence text) {
List<Candidate> results = new LinkedList<>();

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -25,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.components.emoji.Emojifier
private val defaultModifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp)
private val defaultTextStyle: @Composable () -> TextStyle = { MaterialTheme.typography.bodyLarge }
@@ -83,22 +85,28 @@ fun MemberLabelPill(
.then(modifier),
verticalAlignment = Alignment.CenterVertically
) {
if (!emoji.isNullOrEmpty()) {
Text(
text = emoji,
style = textStyle,
modifier = Modifier.padding(end = 5.dp)
)
}
ProvideTextStyle(textStyle) {
if (!emoji.isNullOrEmpty()) {
Emojifier(text = emoji) { annotatedText, inlineContent ->
Text(
text = annotatedText,
inlineContent = inlineContent,
modifier = Modifier.padding(end = 5.dp)
)
}
}
if (text.isNotEmpty()) {
Text(
text = text,
color = textColor,
style = textStyle,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (text.isNotEmpty()) {
Emojifier(text = text) { annotatedText, inlineContent ->
Text(
text = annotatedText,
inlineContent = inlineContent,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -23,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.components.emoji.Emojifier
private val defaultLabelModifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
private val defaultLabelTextStyle: @Composable () -> TextStyle = { MaterialTheme.typography.bodySmall }
@@ -101,14 +103,17 @@ private fun SenderNameWithLabel(
verticalArrangement = Arrangement.spacedBy(2.dp),
itemVerticalAlignment = Alignment.CenterVertically
) {
Text(
text = senderName,
color = senderColor,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
ProvideTextStyle(MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold)) {
Emojifier(text = senderName) { annotatedText, inlineContent ->
Text(
text = annotatedText,
inlineContent = inlineContent,
color = senderColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
if (memberLabel != null) {
labelSlot(memberLabel)

View File

@@ -7,10 +7,13 @@ package org.thoughtcrime.securesms.video.videoconverter.exceptions
class EncodingException : Exception {
/** Whether the input video was HDR content. */
@JvmField var isHdrInput: Boolean = false
/** Whether HDR-to-SDR tone-mapping was successfully applied to the decoder. */
@JvmField var toneMapApplied: Boolean = false
/** The name of the video decoder codec that was selected, or null if decoder creation failed. */
@JvmField var decoderName: String? = null
/** The name of the video encoder codec that was selected, or null if encoder creation failed. */
@JvmField var encoderName: String? = null