diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emojifier.kt b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emojifier.kt index deec4a99d6..d601cc73dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emojifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emojifier.kt @@ -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) -> 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 = 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 = remember(text) { + EmojiProvider.getCandidates(text)?.list.orEmpty() + } + val inlineContentByEmoji: Map = 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 +): 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiParser.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiParser.java index ced9178683..a64c9f28f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiParser.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiParser.java @@ -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 results = new LinkedList<>(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPill.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPill.kt index 92193a766e..3e22219d10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPill.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPill.kt @@ -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 + ) + } + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/SenderNameWithLabel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/SenderNameWithLabel.kt index 3eb0bb7a6c..0f9e4f1e47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/SenderNameWithLabel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/SenderNameWithLabel.kt @@ -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) diff --git a/lib/video/src/main/java/org/thoughtcrime/securesms/video/videoconverter/exceptions/EncodingException.kt b/lib/video/src/main/java/org/thoughtcrime/securesms/video/videoconverter/exceptions/EncodingException.kt index 1f5138adea..e00bdf27aa 100644 --- a/lib/video/src/main/java/org/thoughtcrime/securesms/video/videoconverter/exceptions/EncodingException.kt +++ b/lib/video/src/main/java/org/thoughtcrime/securesms/video/videoconverter/exceptions/EncodingException.kt @@ -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