From c6fe9786a9a76a3d3b5773c28ed57566cdcf940f Mon Sep 17 00:00:00 2001 From: hayyaksi <193020925+hayaksi1@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:23:01 +0300 Subject: [PATCH] Stop rescanning message bodies for emoji only on every recomposition (#7531) Deciding whether a text message is emoji only compares the formatted body to the plain one and runs a grapheme scan over the whole message. Nothing was remembered, so both ran again on every recomposition of every visible message. Remember the decision against the body and the formatted body. The preview branch moves into the same computation, so the composable wrapper around containsOnlyEmojis, which had a single caller and existed only to read LocalInspectionMode, is no longer needed. --- .../components/event/TimelineItemTextView.kt | 12 ++++++-- .../features/messages/impl/utils/Emoji.kt | 12 +------- .../features/messages/impl/utils/EmojiTest.kt | 30 +++++++++---------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index a7d54e819b..86b50f3015 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -15,7 +15,9 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +25,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData +import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_ONLY_TEXT import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentPreviewParam import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent @@ -47,8 +50,13 @@ fun TimelineItemTextView( // The View <-> Compose interop is not working well with Compose UI tests (it loops indefinitely), so we skip it in the UI test mode. if (LocalUiTestMode.current) return - val emojiOnly = content.formattedBody.toString() == content.body && - content.body.replace(" ", "").containsOnlyEmojis() + val isInPreview = LocalInspectionMode.current + val emojiOnly = remember(content.body, content.formattedBody, isInPreview) { + content.formattedBody.toString() == content.body && + content.body.replace(" ", "").let { body -> + if (isInPreview) body == AN_EMOJI_ONLY_TEXT else body.containsOnlyEmojis() + } + } val textStyle = when { emojiOnly -> ElementTheme.typography.fontHeadingXlRegular else -> ElementTheme.typography.fontBodyLgRegular diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt index 3a0f16b3e9..68019f0c04 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt @@ -8,25 +8,15 @@ package io.element.android.features.messages.impl.utils -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalInspectionMode import com.sigpwned.emoji4j.core.Grapheme.Type.EMOJI import com.sigpwned.emoji4j.core.Grapheme.Type.PICTOGRAPHIC import com.sigpwned.emoji4j.core.GraphemeMatchResult import com.sigpwned.emoji4j.core.GraphemeMatcher -import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_ONLY_TEXT /** * Returns true if the string consists exclusively of "emoji or pictographic graphemes". */ -@Composable -fun String.containsOnlyEmojis(): Boolean { - if (LocalInspectionMode.current) return this == AN_EMOJI_ONLY_TEXT - if (isEmpty()) return false - return containsOnlyEmojisInternal() -} - -internal fun String.containsOnlyEmojisInternal(): Boolean { +internal fun String.containsOnlyEmojis(): Boolean { val matcher = GraphemeMatcher(this) var m: GraphemeMatchResult? = null var contiguous = true diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt index 5c979dbb7c..e75eb83d62 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt @@ -16,23 +16,23 @@ class EmojiTest { @Test fun validEmojis() { // Simple single/multiple single-codepoint emojis per string - assertTrue("👍".containsOnlyEmojisInternal()) - assertTrue("😀".containsOnlyEmojisInternal()) - assertTrue("🙂🙁".containsOnlyEmojisInternal()) - assertTrue("đŸ‘â¤ī¸đŸ".containsOnlyEmojisInternal()) // 👁 is a pictographic - assertTrue("👨‍👩‍đŸ‘Ļ1ī¸âƒŖđŸš€đŸ‘ŗđŸžâ€â™‚ī¸đŸĒŠ".containsOnlyEmojisInternal()) - assertTrue("🌍🌎🌏".containsOnlyEmojisInternal()) + assertTrue("👍".containsOnlyEmojis()) + assertTrue("😀".containsOnlyEmojis()) + assertTrue("🙂🙁".containsOnlyEmojis()) + assertTrue("đŸ‘â¤ī¸đŸ".containsOnlyEmojis()) // 👁 is a pictographic + assertTrue("👨‍👩‍đŸ‘Ļ1ī¸âƒŖđŸš€đŸ‘ŗđŸžâ€â™‚ī¸đŸĒŠ".containsOnlyEmojis()) + assertTrue("🌍🌎🌏".containsOnlyEmojis()) // Awkward multi-codepoint graphemes - assertTrue("🧑‍🧑‍🧒‍🧒".containsOnlyEmojisInternal()) - assertTrue("🏴‍☠".containsOnlyEmojisInternal()) - assertTrue("👩đŸŋ‍🔧".containsOnlyEmojisInternal()) + assertTrue("🧑‍🧑‍🧒‍🧒".containsOnlyEmojis()) + assertTrue("🏴‍☠".containsOnlyEmojis()) + assertTrue("👩đŸŋ‍🔧".containsOnlyEmojis()) - Assert.assertFalse("".containsOnlyEmojisInternal()) - Assert.assertFalse(" ".containsOnlyEmojisInternal()) - Assert.assertFalse("🙂 🙁".containsOnlyEmojisInternal()) - Assert.assertFalse(" 🙂 🙁 ".containsOnlyEmojisInternal()) - Assert.assertFalse("Hello".containsOnlyEmojisInternal()) - Assert.assertFalse("Hello 👋".containsOnlyEmojisInternal()) + Assert.assertFalse("".containsOnlyEmojis()) + Assert.assertFalse(" ".containsOnlyEmojis()) + Assert.assertFalse("🙂 🙁".containsOnlyEmojis()) + Assert.assertFalse(" 🙂 🙁 ".containsOnlyEmojis()) + Assert.assertFalse("Hello".containsOnlyEmojis()) + Assert.assertFalse("Hello 👋".containsOnlyEmojis()) } }