From d0b66f5b42cbf7c6a5c6fc85bc6651aef4f3fe5b Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Tue, 8 Sep 2026 11:08:38 +0200 Subject: [PATCH 1/3] Render message text as markdown in the Compose UI kit --- gradle/libs.versions.toml | 2 + .../compose/sample/data/CustomSettings.kt | 5 + .../compose/sample/ui/MessagesActivity.kt | 18 + .../sample/ui/login/CustomLoginActivity.kt | 25 +- .../src/main/res/values/strings.xml | 2 + .../api/stream-chat-android-compose.api | 1 + stream-chat-android-compose/build.gradle.kts | 3 + .../ui/components/messages/MessageText.kt | 33 +- .../compose/ui/util/AnnotatedString.kt | 19 + .../compose/ui/util/BlockQuoteRails.kt | 97 +++ .../ui/util/MarkdownMessageTextFormatter.kt | 83 +++ .../android/compose/ui/util/MarkdownStyles.kt | 84 +++ .../compose/ui/util/MessageTextFormatter.kt | 60 +- .../android/compose/ui/util/MessageUtils.kt | 11 + .../chat/android/compose/ui/util/TextUtils.kt | 110 ++- .../ui/util/internal/MarkdownEmitter.kt | 188 ++++++ .../ui/util/internal/MarkdownRenderer.kt | 632 ++++++++++++++++++ .../util/MarkdownMessageTextFormatterTest.kt | 210 ++++++ .../compose/ui/util/MarkdownRendererTest.kt | 430 ++++++++++++ .../ui/util/MarkdownRtlSnapshotTest.kt | 100 +++ .../compose/ui/util/MarkdownSnapshotTest.kt | 155 +++++ ...nRtlSnapshotTest_right_to_left_content.png | Bin 0 -> 94588 bytes ...SnapshotTest_every_supported_construct.png | Bin 0 -> 47485 bytes ...nSnapshotTest_quote_inside_a_list_item.png | Bin 0 -> 15850 bytes ..._MarkdownSnapshotTest_wrapping_content.png | Bin 0 -> 31738 bytes 25 files changed, 2222 insertions(+), 46 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/BlockQuoteRails.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatter.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownStyles.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownEmitter.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownRenderer.kt create mode 100644 stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatterTest.kt create mode 100644 stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRendererTest.kt create mode 100644 stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRtlSnapshotTest.kt create mode 100644 stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownSnapshotTest.kt create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownRtlSnapshotTest_right_to_left_content.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownSnapshotTest_every_supported_construct.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownSnapshotTest_quote_inside_a_list_item.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownSnapshotTest_wrapping_content.png diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7352f655d4c..94e591237ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,6 +62,7 @@ ksp = "2.0.21-1.0.28" landscapist = "2.4.7" leakCanary = "2.4" macroBenchmark = "1.2.3" +markdown = "0.7.3" markwon = "4.6.2" materialComponents = "1.12.0" mockitoKotlin = "5.4.0" @@ -182,6 +183,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines"} kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin"} leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakCanary"} +markdown = { module = "org.jetbrains:markdown", version.ref = "markdown"} markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon"} markwon-ext-strikethrough = { module = "io.noties.markwon:ext-strikethrough", version.ref = "markwon"} markwon-linkify = { module = "io.noties.markwon:linkify", version.ref = "markwon"} diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt index deb0a84a3f6..f3435aa5f3c 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt @@ -32,8 +32,13 @@ class CustomSettings(private val context: Context) { get() = prefs.getBoolean(SETTINGS_KEY_ADAPTIVE_LAYOUT, false) set(value) = prefs.edit().putBoolean(SETTINGS_KEY_ADAPTIVE_LAYOUT, value).apply() + var isMarkdownEnabled: Boolean + get() = prefs.getBoolean(SETTINGS_KEY_MARKDOWN, false) + set(value) = prefs.edit().putBoolean(SETTINGS_KEY_MARKDOWN, value).apply() + companion object { private const val SETTINGS_KEY_ADAPTIVE_LAYOUT = "adaptive_layout" + private const val SETTINGS_KEY_MARKDOWN = "markdown" } } diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt index b5866259a47..0488f340c1b 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import io.getstream.chat.android.compose.sample.ChatApp import io.getstream.chat.android.compose.sample.R +import io.getstream.chat.android.compose.sample.data.customSettings import io.getstream.chat.android.compose.sample.feature.channel.isGroupChannel import io.getstream.chat.android.compose.sample.ui.channel.DirectChannelInfoActivity import io.getstream.chat.android.compose.sample.ui.channel.GroupChannelInfoActivity @@ -89,6 +90,7 @@ import io.getstream.chat.android.compose.ui.theme.ReactionOptionsTheme import io.getstream.chat.android.compose.ui.theme.StreamColors import io.getstream.chat.android.compose.ui.theme.StreamShapes import io.getstream.chat.android.compose.ui.theme.StreamTypography +import io.getstream.chat.android.compose.ui.util.MessageTextFormatter import io.getstream.chat.android.compose.ui.util.rememberMessageListState import io.getstream.chat.android.compose.viewmodel.messages.AttachmentsPickerViewModel import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel @@ -157,11 +159,27 @@ class MessagesActivity : ComponentActivity() { val ownMessageTheme = MessageTheme.defaultOwnTheme(isInDarkMode, typography, shapes, colors) val attachmentsPickerTabFactories = AttachmentsPickerTabFactories.defaultFactories() + LocationPickerTabFactory(viewModelFactory = SharedLocationViewModelFactory(cid)) + val messageTextFormatter = when { + customSettings().isMarkdownEnabled -> MessageTextFormatter.markdownFormatter( + autoTranslationEnabled = ChatApp.autoTranslationEnabled, + isInDarkMode = isInDarkMode, + typography = typography, + colors = colors, + ) + else -> MessageTextFormatter.defaultFormatter( + autoTranslationEnabled = ChatApp.autoTranslationEnabled, + isInDarkMode = isInDarkMode, + typography = typography, + shapes = shapes, + colors = colors, + ) + } ChatTheme( isInDarkMode = isInDarkMode, colors = colors, shapes = shapes, typography = typography, + messageTextFormatter = messageTextFormatter, attachmentsPickerTabFactories = attachmentsPickerTabFactories, componentFactory = CustomChatComponentFactory(), dateFormatter = ChatApp.dateFormatter, diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt index cc4f4b9d8f2..3bde2a0bd65 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.activity.compose.setContent +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -125,6 +126,7 @@ class CustomLoginActivity : AppCompatActivity() { var userTokenText by remember { mutableStateOf("") } var userNameText by remember { mutableStateOf("") } var isAdaptiveLayoutEnabled by remember { mutableStateOf(settings.isAdaptiveLayoutEnabled) } + var isMarkdownEnabled by remember { mutableStateOf(settings.isMarkdownEnabled) } val isLoginButtonEnabled = apiKeyText.isNotEmpty() && userIdText.isNotEmpty() && @@ -134,6 +136,10 @@ class CustomLoginActivity : AppCompatActivity() { settings.isAdaptiveLayoutEnabled = isAdaptiveLayoutEnabled } + LaunchedEffect(isMarkdownEnabled) { + settings.isMarkdownEnabled = isMarkdownEnabled + } + CustomLoginInputField( hint = stringResource(id = R.string.custom_login_hint_api_key), value = apiKeyText, @@ -160,11 +166,20 @@ class CustomLoginActivity : AppCompatActivity() { HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) - EnableAdaptiveScreenField( + FeatureFlagField( + title = R.string.custom_login_enable_adaptive_layout, + description = R.string.custom_login_enable_adaptive_layout_description, value = isAdaptiveLayoutEnabled, onValueChange = { isChecked -> isAdaptiveLayoutEnabled = isChecked }, ) + FeatureFlagField( + title = R.string.custom_login_enable_markdown, + description = R.string.custom_login_enable_markdown_description, + value = isMarkdownEnabled, + onValueChange = { isChecked -> isMarkdownEnabled = isChecked }, + ) + Spacer(modifier = Modifier.weight(1f)) CustomLoginButton( @@ -272,7 +287,9 @@ class CustomLoginActivity : AppCompatActivity() { } @Composable - private fun EnableAdaptiveScreenField( + private fun FeatureFlagField( + @StringRes title: Int, + @StringRes description: Int, value: Boolean, onValueChange: (Boolean) -> Unit, ) { @@ -293,11 +310,11 @@ class CustomLoginActivity : AppCompatActivity() { ) Column { Text( - text = stringResource(id = R.string.custom_login_enable_adaptive_layout), + text = stringResource(id = title), style = ChatTheme.typography.title3, ) Text( - text = stringResource(id = R.string.custom_login_enable_adaptive_layout_description), + text = stringResource(id = description), style = ChatTheme.typography.footnote, ) } diff --git a/stream-chat-android-compose-sample/src/main/res/values/strings.xml b/stream-chat-android-compose-sample/src/main/res/values/strings.xml index 91f605fab48..bfbbf8b966d 100644 --- a/stream-chat-android-compose-sample/src/main/res/values/strings.xml +++ b/stream-chat-android-compose-sample/src/main/res/values/strings.xml @@ -35,6 +35,8 @@ Username (optional) Enable adaptive layout (Experimental) Adjust layout based on screen sizes + Enable markdown + Render message text as markdown Pinned Messages diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index a10b9110604..13e7b34fe8e 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -4648,6 +4648,7 @@ public final class io/getstream/chat/android/compose/ui/util/MessageTextFormatte public final fun composite ([Lio/getstream/chat/android/compose/ui/util/MessageTextFormatter;)Lio/getstream/chat/android/compose/ui/util/MessageTextFormatter; public final fun defaultFormatter (ZZLio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)Lio/getstream/chat/android/compose/ui/util/MessageTextFormatter; public final fun defaultFormatter (ZZLio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/MessageTheme;Lio/getstream/chat/android/compose/ui/theme/MessageTheme;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)Lio/getstream/chat/android/compose/ui/util/MessageTextFormatter; + public final fun markdownFormatter (ZZLio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Landroidx/compose/runtime/Composer;II)Lio/getstream/chat/android/compose/ui/util/MessageTextFormatter; } public final class io/getstream/chat/android/compose/ui/util/MessageUtilsKt { diff --git a/stream-chat-android-compose/build.gradle.kts b/stream-chat-android-compose/build.gradle.kts index 45baf3beed7..848181a93dd 100644 --- a/stream-chat-android-compose/build.gradle.kts +++ b/stream-chat-android-compose/build.gradle.kts @@ -95,6 +95,9 @@ dependencies { implementation(libs.coil.gif) implementation(libs.coil.video) + // Markdown + implementation(libs.markdown) + // Media3 implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt index 43eaaeb7bcd..84f65f8510c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.compose.ui.components.messages +import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import androidx.compose.foundation.gestures.detectTapGestures @@ -42,6 +43,8 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.AnnotationTagEmail import io.getstream.chat.android.compose.ui.util.AnnotationTagMention import io.getstream.chat.android.compose.ui.util.AnnotationTagUrl +import io.getstream.chat.android.compose.ui.util.MarkdownStyles +import io.getstream.chat.android.compose.ui.util.blockQuoteRails import io.getstream.chat.android.compose.ui.util.isEmojiOnlyWithoutBubble import io.getstream.chat.android.compose.ui.util.isFewEmoji import io.getstream.chat.android.compose.ui.util.isSingleEmoji @@ -89,7 +92,16 @@ public fun MessageText( } } - val annotations = styledText.getStringAnnotations(0, styledText.lastIndex) + val annotations = styledText.getStringAnnotations(0, styledText.length) + + // Read inside the draw pass, which runs after the layout that sets it. + val layout = remember(styledText) { mutableStateOf(null) } + val quoteRails = Modifier.blockQuoteRails( + annotations = annotations, + layout = layout::value, + color = ChatTheme.colors.textLowEmphasis, + indentPerDepth = MarkdownStyles.BlockQuoteIndent, + ) // TODO: Fix emoji font padding once this is resolved and exposed: https://issuetracker.google.com/issues/171394808 val style = when { @@ -113,10 +125,12 @@ public fun MessageText( top = 8.dp, bottom = 8.dp, ) - .testTag("Stream_MessageClickableText"), + .testTag("Stream_MessageClickableText") + .then(quoteRails), text = styledText, style = style, onLongPress = { onLongItemClick(message) }, + onTextLayout = { layout.value = it }, ) { position -> val annotation = annotations.firstOrNull { position in it.start..it.end } if (annotation?.tag == AnnotationTagMention) { @@ -125,9 +139,14 @@ public fun MessageText( val targetUrl = annotation?.item if (!targetUrl.isNullOrEmpty()) { onLinkClick?.invoke(message, targetUrl) ?: run { - context.startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(targetUrl)), - ) + try { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(targetUrl)), + ) + } catch (_: ActivityNotFoundException) { + // Nothing guarantees an app exists for the link's scheme, and a tap on + // one must not bring the message list down. + } } } } @@ -142,9 +161,11 @@ public fun MessageText( vertical = verticalPadding, ) .clipToBounds() - .testTag("Stream_MessageText"), + .testTag("Stream_MessageText") + .then(quoteRails), text = styledText, style = style, + onTextLayout = { layout.value = it }, ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/AnnotatedString.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/AnnotatedString.kt index d7216f21712..dd534a8abe8 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/AnnotatedString.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/AnnotatedString.kt @@ -76,3 +76,22 @@ internal fun AnnotatedString.Builder.merge(annotated: AnnotatedString) { addTtsAnnotations(annotated.ttsAnnotations) addUrlAnnotations(annotated.urlAnnotations) } + +/** + * The text with a line feed wherever a paragraph starts one, since a paragraph break is a line the + * layout draws rather than a character, and an announcement can only read characters. + */ +internal fun AnnotatedString.textWithParagraphBreaks(): String { + // Unconditionally: a paragraph break renders a line of its own, on top of any line feed + // already sitting in front of it, which is how a blank line between two of them is made. + val starts = paragraphStyles + .map { it.start } + .filterTo(mutableSetOf()) { it > 0 } + if (starts.isEmpty()) return text + return buildString(text.length + starts.size) { + text.forEachIndexed { index, character -> + if (index in starts) append('\n') + append(character) + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/BlockQuoteRails.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/BlockQuoteRails.kt new file mode 100644 index 00000000000..136770a764f --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/BlockQuoteRails.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.style.ResolvedTextDirection +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp + +/** + * Draws the rail beside every rendered line of a block quote, in [color], reading the quotes from + * the [AnnotationTagBlockQuote] ranges of [annotations] and their extent from [layout]. + * + * Drawn rather than written as a marker character, because the lines a quote occupies are only + * known once the text has been laid out. A character can be placed on a line break the renderer + * made, never on one the layout chose, and it leaves a gap between lines besides. + */ +internal fun Modifier.blockQuoteRails( + annotations: List>, + layout: () -> TextLayoutResult?, + color: Color, + indentPerDepth: TextUnit, +): Modifier { + val quotes = annotations.filter { it.tag == AnnotationTagBlockQuote } + if (quotes.isEmpty()) return this + return drawBehind { + val laidOut = layout() ?: return@drawBehind + val step = indentPerDepth.toPx(this, laidOut) + val width = RailWidth.toPx() + quotes.forEach { quote -> + val depth = quote.item.toIntOrNull() ?: return@forEach + // Centred in the space the last level of indent opened up. + val offset = step * (depth - 1) + (step - width) / 2 + val lines = laidOut.lineRange(quote) ?: return@forEach + // Mirrored for a quote running right to left, since the indent it sits in is + // start-relative. Taken from the paragraph rather than the layout direction, because + // one message can carry a quote of each direction. + val left = when (laidOut.getParagraphDirection(quote.start)) { + ResolvedTextDirection.Rtl -> size.width - offset - width + else -> offset + } + for (line in lines) { + val top = laidOut.getLineTop(line) + drawRect( + color = color, + topLeft = Offset(left, top), + size = Size(width, laidOut.getLineBottom(line) - top), + ) + } + } + } +} + +/** + * Every line the quote occupies, blank ones included, so the rail runs unbroken through the gap + * between two paragraphs of the same quote. + */ +private fun TextLayoutResult.lineRange(quote: AnnotatedString.Range): IntRange? { + val last = (quote.end - 1).coerceAtLeast(quote.start) + if (quote.start >= layoutInput.text.length) return null + return getLineForOffset(quote.start)..getLineForOffset(last.coerceAtMost(layoutInput.text.length - 1)) +} + +/** Resolves against the laid-out font size, since the indent is expressed relative to the text. */ +private fun TextUnit.toPx(density: Density, layout: TextLayoutResult): Float { + val fontSize = layout.layoutInput.style.fontSize + return when { + type == TextUnitType.Sp -> with(density) { toPx() } + type == TextUnitType.Em && fontSize.type == TextUnitType.Sp -> + value * with(density) { fontSize.toPx() } + else -> 0f + } +} + +private val RailWidth = 2.dp diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatter.kt new file mode 100644 index 00000000000..019a3937a45 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatter.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import io.getstream.chat.android.compose.ui.theme.StreamTypography +import io.getstream.chat.android.compose.ui.util.internal.MarkdownRenderer +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.common.utils.extensions.isMine + +/** + * Renders message text as markdown. Built by + * [MessageTextFormatter.markdownFormatter], which documents what is and is not supported. + */ +@Suppress("LongParameterList") +internal class MarkdownMessageTextFormatter( + private val autoTranslationEnabled: Boolean, + private val typography: StreamTypography, + private val styles: MarkdownStyles, + private val textStyle: (isMine: Boolean, message: Message) -> TextStyle, + private val linkStyle: (isMine: Boolean) -> TextStyle, + private val mentionColor: (isMine: Boolean) -> Color, + private val builder: AnnotatedMessageTextBuilder?, +) : MessageTextFormatter { + + private val renderer = MarkdownRenderer(styles) + + override fun format(message: Message, currentUser: User?): AnnotatedString { + val displayedText = message.resolveDisplayedText(currentUser, autoTranslationEnabled) + val isMine = message.isMine(currentUser) + val baseStyle = SpanStyle( + fontStyle = typography.body.fontStyle, + color = textStyle(isMine, message).color, + ) + val markdown = renderer.render(displayedText) + // Colour only: a full text style would overwrite the weight and size markdown set, both + // for the links markdown carried and for the ones the entity pass detects. + val link = linkStyle(isMine).let { TextStyle(color = it.color, textDecoration = it.textDecoration) } + return buildAnnotatedString { + append(markdown.text) + // The base style goes on first so the markdown spans layered over it win. + addStyle(baseStyle, start = 0, end = markdown.text.length) + addParagraphStyles(markdown.paragraphStyles) + markdown.spanStyles.forEach { addStyle(it.item, it.start, it.end) } + markdown.getStringAnnotations(0, markdown.length).forEach { + addStringAnnotation(it.tag, it.item, it.start, it.end) + // The entity pass below skips annotated ranges, so markdown links style here. + if (it.tag == AnnotationTagUrl) addStyle(link.toSpanStyle(), it.start, it.end) + } + } + .annotateStreamEntities( + mentionedUserNames = message.mentionedUsers.map { it.name.ifEmpty { it.id } }, + linkStyle = link, + mentionsColor = mentionColor(isMine), + ) + .let { annotated -> + val extra = builder ?: return@let annotated + buildAnnotatedString { + append(annotated) + extra.invoke(this, message, currentUser) + } + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownStyles.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownStyles.kt new file mode 100644 index 00000000000..7a11e0540aa --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MarkdownStyles.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.em +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamColors +import io.getstream.chat.android.compose.ui.theme.StreamTypography + +/** + * Styling for the markdown constructs rendered inside a message bubble. + * + * @param listIndent Indents a list once per level of nesting, wrapped lines included. + * @param blockQuoteIndent Sets a quote in from the margin once per level, leaving room for + * the rail drawn beside it. + * @param thematicBreak Stands in for a thematic break (`---`). + */ +@Immutable +internal data class MarkdownStyles( + val headings: List, + val codeSpan: SpanStyle, + val codeBlock: SpanStyle, + val blockQuote: SpanStyle, + val listIndent: TextUnit = 1.em, + val blockQuoteIndent: TextUnit = BlockQuoteIndent, + val thematicBreak: String = "⸻", +) { + + /** The style for a heading of the given [level], counting from 1. */ + fun heading(level: Int): SpanStyle = headings.getOrElse(level - 1) { headings.last() } + + companion object { + + /** Shared with whatever draws the rail, so the two agree on where it goes. */ + val BlockQuoteIndent: TextUnit = 1.em + + /** Builds the default styling from the design system. */ + @Composable + fun defaults( + typography: StreamTypography = ChatTheme.typography, + colors: StreamColors = ChatTheme.colors, + ): MarkdownStyles = MarkdownStyles( + // Four sizes for six levels, so two pairs collide. The fourth stays at body size, + // as it does on the web, and the sixth is set apart by colour. Every level is + // weighted above body text, which is what carries a heading where sizes run out. + headings = listOf( + typography.title1.toSpanStyle(), + typography.title3Bold.toSpanStyle(), + typography.bodyBold.toSpanStyle(), + typography.bodyBold.toSpanStyle(), + typography.footnoteBold.toSpanStyle(), + typography.footnoteBold.toSpanStyle().copy(color = colors.textLowEmphasis), + ), + codeSpan = SpanStyle( + fontFamily = FontFamily.Monospace, + background = colors.inputBackground, + ), + codeBlock = SpanStyle( + fontFamily = FontFamily.Monospace, + background = colors.inputBackground, + ), + blockQuote = SpanStyle(color = colors.textLowEmphasis), + ) + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt index c9663b3eaa5..a518c84d147 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageTextFormatter.kt @@ -28,7 +28,6 @@ import io.getstream.chat.android.compose.ui.theme.StreamShapes import io.getstream.chat.android.compose.ui.theme.StreamTypography import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User -import io.getstream.chat.android.ui.common.feature.messages.translations.MessageOriginalTranslationsStore import io.getstream.chat.android.ui.common.utils.extensions.isMine /** @@ -137,6 +136,48 @@ public fun interface MessageTextFormatter { ) } + /** + * Builds a formatter that renders the message text as markdown, in place of + * [defaultFormatter]: + * ``` + * ChatTheme( + * messageTextFormatter = MessageTextFormatter.markdownFormatter(autoTranslationEnabled = true), + * ) + * ``` + * + * Mentions, links and emails are highlighted on top of the rendered markdown. Rendering + * changes the text's length, so offsets no longer line up with [Message.text] and this + * cannot be combined through [composite], which styles by those offsets. Styling follows + * [typography] and [colors]. + * + * A single line break renders as a line break, where the specification collapses it to a + * space. Complying would leave two trailing spaces as the only way to write one, and would + * reflow every multi-line message that reads correctly as plain text today. The View-based + * kit deviates the same way. + * + * Emphasis, strikethrough, code, headings, lists, quotes and links are rendered. An image + * falls back to its alt text, and a table or task list to its source, since none of the + * three can be drawn in a styled string. + */ + @Composable + public fun markdownFormatter( + autoTranslationEnabled: Boolean, + isInDarkMode: Boolean = isSystemInDarkTheme(), + typography: StreamTypography = StreamTypography.defaultTypography(), + colors: StreamColors = when (isInDarkMode) { + true -> StreamColors.defaultDarkColors() + else -> StreamColors.defaultColors() + }, + ): MessageTextFormatter = MarkdownMessageTextFormatter( + autoTranslationEnabled = autoTranslationEnabled, + typography = typography, + styles = MarkdownStyles.defaults(typography = typography, colors = colors), + textStyle = defaultTextStyle(isInDarkMode, typography, colors), + linkStyle = defaultLinkStyle(colors), + mentionColor = defaultMentionColor(isInDarkMode, typography, colors), + builder = null, + ) + /** * Builds a composite message text formatter. * @@ -185,22 +226,7 @@ private class DefaultMessageTextFormatter( ) : MessageTextFormatter { override fun format(message: Message, currentUser: User?): AnnotatedString { - val displayedText = when (autoTranslationEnabled) { - true -> { - // If auto-translation is enabled, we check if the message is showing original text. - // If it is, we return the original text, otherwise we return the translated text. - if (MessageOriginalTranslationsStore.forChannel(message.cid).shouldShowOriginalText(message.id)) { - message.text - } else { - // If the message is not showing original text, we check if the current user has a language set. - // If they do, we return the translated text, otherwise we return the original text. - currentUser?.language?.let { userLanguage -> - message.getTranslation(userLanguage).ifEmpty { message.text } - } ?: message.text - } - } - else -> message.text - } + val displayedText = message.resolveDisplayedText(currentUser, autoTranslationEnabled) val mentionedUserNames = message.mentionedUsers.map { it.name.ifEmpty { it.id } } val isMine = message.isMine(currentUser) val textColor = textStyle(isMine, message).color diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt index 4ff8c06da4c..00bb1de376c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt @@ -93,3 +93,14 @@ internal fun Message.isEmojiOnlyWithoutBubble(): Boolean = isFewEmoji() && * Max number of emoji without showing it inside a bubble. */ internal const val MaxFullSizeEmoji: Int = 3 + +/** + * The text to display for this message, honouring auto-translation and the per-message "show + * original text" toggle. [currentUser]'s language selects the translation. + */ +internal fun Message.resolveDisplayedText(currentUser: User?, autoTranslationEnabled: Boolean): String { + if (!autoTranslationEnabled) return text + if (MessageOriginalTranslationsStore.forChannel(cid).shouldShowOriginalText(id)) return text + val userLanguage = currentUser?.language ?: return text + return getTranslation(userLanguage).ifEmpty { text } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt index 7408d4b1865..e1e3495b69e 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt @@ -48,6 +48,15 @@ internal const val AnnotationTagEmail: AnnotationTag = "EMAIL" */ internal const val AnnotationTagMention: AnnotationTag = "MENTION" +/** + * The tag marking text to take literally, such as markdown code, so that nothing inside it is + * detected as a URL, an email or a mention. + */ +internal const val AnnotationTagLiteral: AnnotationTag = "LITERAL" + +/** Marks a block quote's span, carrying its depth of nesting, so its rail can be drawn. */ +internal const val AnnotationTagBlockQuote: AnnotationTag = "BLOCK_QUOTE" + /** * Builds an [AnnotatedString] from a given text, applying styles and annotations for links and mentions. * Used in message bubbles. @@ -82,25 +91,9 @@ internal fun buildAnnotatedMessageText( end = text.length, ) - // Then for each available link in the text, we add a different style, to represent the links, - // as well as add a String annotation to it. This gives us the ability to open the URL on click. - linkify( - text = text, - tag = AnnotationTagUrl, - pattern = PatternsCompat.AUTOLINK_WEB_URL, - matchFilter = Linkify.sUrlMatchFilter, - schemes = URL_SCHEMES, - textStyle = linkStyle, - ) - linkify( - text = text, - tag = AnnotationTagEmail, - pattern = PatternsCompat.AUTOLINK_EMAIL_ADDRESS, - schemes = EMAIL_SCHEMES, - textStyle = linkStyle, - ) - tagUser( + annotateEntities( text = text, + linkStyle = linkStyle, mentionsColor = mentionsColor, mentionedUserNames = mentionedUserNames, ) @@ -110,6 +103,80 @@ internal fun buildAnnotatedMessageText( } } +/** + * Adds the annotations Stream recognises in message text, URLs, emails and the mentions named by + * [mentionedUserNames], on top of an already styled string. + * + * Ranges already tagged [AnnotationTagUrl] keep the destination they were built with, and ranges + * tagged [AnnotationTagLiteral] are left as written. + * + * @param mentionedUserNames The names to highlight, each without its leading `@`. + * @param linkStyle The style applied to URLs and emails. + * @param mentionsColor Applied to every mention. + */ +internal fun AnnotatedString.annotateStreamEntities( + mentionedUserNames: List, + linkStyle: TextStyle, + mentionsColor: Color, +): AnnotatedString { + val styled = this + return buildAnnotatedString { + append(styled.text) + addSpanStyles(styled.spanStyles) + addParagraphStyles(styled.paragraphStyles) + // Everything but the literal markers, which exist only for the pass below. + addStringAnnotations(styled.stringAnnotations.filter { it.tag != AnnotationTagLiteral }) + annotateEntities( + text = styled.text, + linkStyle = linkStyle, + mentionsColor = mentionsColor, + mentionedUserNames = mentionedUserNames, + skipRanges = styled.stringAnnotations + .filter { it.tag == AnnotationTagUrl || it.tag == AnnotationTagLiteral } + .map { it.start until it.end }, + ) + } +} + +/** + * Styles and annotates every URL, email and mention in [text], which the receiver must already + * hold as its content for the match offsets to line up. + */ +@SuppressLint("RestrictedApi") +private fun AnnotatedString.Builder.annotateEntities( + text: String, + linkStyle: TextStyle, + mentionsColor: Color, + mentionedUserNames: List, + skipRanges: List = emptyList(), +) { + // For each available link in the text, we add a different style, to represent the links, + // as well as add a String annotation to it. This gives us the ability to open the URL on click. + linkify( + text = text, + tag = AnnotationTagUrl, + pattern = PatternsCompat.AUTOLINK_WEB_URL, + matchFilter = Linkify.sUrlMatchFilter, + schemes = URL_SCHEMES, + textStyle = linkStyle, + skipRanges = skipRanges, + ) + linkify( + text = text, + tag = AnnotationTagEmail, + pattern = PatternsCompat.AUTOLINK_EMAIL_ADDRESS, + schemes = EMAIL_SCHEMES, + textStyle = linkStyle, + skipRanges = skipRanges, + ) + tagUser( + text = text, + mentionsColor = mentionsColor, + mentionedUserNames = mentionedUserNames, + skipRanges = skipRanges, + ) +} + /** * Builds an [AnnotatedString] from a given text, applying styles and annotations for links and mentions. * Used in message input fields. @@ -200,6 +267,7 @@ private fun AnnotatedString.Builder.linkify( matchFilter: Linkify.MatchFilter? = null, schemes: List, textStyle: TextStyle, + skipRanges: List = emptyList(), ) { @SuppressLint("RestrictedApi") val matcher = pattern.matcher(text) @@ -207,7 +275,9 @@ private fun AnnotatedString.Builder.linkify( val start = matcher.start() val end = matcher.end() - if (matchFilter != null && !matchFilter.acceptMatch(text, start, end)) { + val rejected = (matchFilter != null && !matchFilter.acceptMatch(text, start, end)) || + skipRanges.any { start <= it.last && it.first < end } + if (rejected) { continue } @@ -234,12 +304,14 @@ private fun AnnotatedString.Builder.tagUser( text: String, mentionsColor: Color, mentionedUserNames: List, + skipRanges: List = emptyList(), ) { mentionedUserNames.forEach { userName -> val start = text.indexOf(userName) val end = start + userName.length if (start < 0) return@forEach + if (skipRanges.any { start <= it.last && it.first < end }) return@forEach addStyle( style = SpanStyle( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownEmitter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownEmitter.kt new file mode 100644 index 00000000000..7810a2018f1 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownEmitter.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util.internal + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextIndent +import androidx.compose.ui.unit.TextUnit + +/** + * Collects text, styles and annotations while the markdown tree is walked, then assembles them into + * an [AnnotatedString]. + * + * Styles are recorded against offsets rather than pushed and popped, because block constructs are + * styled only once their whole content has been emitted. + */ +internal class MarkdownEmitter { + + private val text = StringBuilder() + private var linePrefix = "" + private val spanStyles = mutableListOf>() + private val annotations = mutableListOf>() + private var indent = TextUnit.Unspecified + private val paragraphStarts = mutableMapOf() + + val length: Int get() = text.length + + fun append(value: CharSequence) { + text.append(value) + } + + /** + * Ends the current line and opens the next with [linePrefix], so a construct marking every one + * of its lines keeps doing so. Every call breaks, so two breaks in the source stay two. + */ + fun appendLineBreak() { + openLine() + } + + val currentLinePrefix: String get() = linePrefix + + /** + * Whether lines currently open with a prefix. While they do, blocks stay separated by + * characters, so the prefix reaches the blank line between two of them; a paragraph break + * would leave that line bare and cut the marker in two. + */ + private val linesArePrefixed: Boolean get() = linePrefix.isNotEmpty() + + /** + * Indents every line [block] emits by [indent], including the ones the layout wraps, so a + * wrapped line keeps the horizontal position its own item started at. + */ + fun withIndent(indent: TextUnit, block: () -> Unit) { + val previous = this.indent + this.indent = indent + try { + block() + } finally { + this.indent = previous + } + } + + /** + * Breaks the line by starting a new paragraph, which is also what carries the indent. Adds no + * line feed of its own, since a paragraph break already renders as one line break; one here + * as well would leave a blank line between the two paragraphs. + */ + fun startParagraph() { + paragraphStarts[text.length] = indent + } + + /** Marks every line [block] emits with [prefix], as a block quote marks its whole span. */ + fun withLinePrefix(prefix: String, block: () -> Unit) { + val previous = linePrefix + linePrefix = prefix + try { + block() + } finally { + linePrefix = previous + } + } + + private fun openLine() { + text.append('\n').append(linePrefix) + } + + fun addSpan(style: SpanStyle, start: Int, end: Int = length) { + if (end > start) spanStyles += AnnotatedString.Range(style, start, end) + } + + fun addAnnotation(tag: String, value: String, start: Int, end: Int = length) { + if (end > start) annotations += AnnotatedString.Range(value, start, end, tag) + } + + /** + * Separates the block just emitted from the next with [newlines] breaks, counting those already + * present. Does nothing while the output is empty, so it never starts with a blank line. + * + * A paragraph break renders as one line break by itself, so it stands in for the first of the + * breaks rather than being added on top of them. + */ + fun endBlock(newlines: Int) { + if (text.isEmpty()) return + val breaksParagraph = !linesArePrefixed + val literal = if (breaksParagraph) newlines - 1 else newlines + var present = 0 + var end = text.length + val opening = "\n$linePrefix" + while (end >= opening.length && text.substring(end - opening.length, end) == opening) { + present++ + end -= opening.length + } + repeat((literal - present).coerceAtLeast(0)) { openLine() } + // Before the prefix, so the prefix opens the next paragraph instead of closing the last. + if (breaksParagraph) paragraphStarts[text.length - linePrefix.length] = indent + } + + /** Drops trailing blank lines, so a trailing block separator does not pad the bubble. */ + fun trimTrailingNewlines() { + val opening = "\n$linePrefix" + while (text.isNotEmpty()) { + when { + text.endsWith(opening) -> text.setLength(text.length - opening.length) + text.last() == '\n' -> text.setLength(text.length - 1) + else -> break + } + } + clampAll() + } + + private fun clampAll() { + clamp(spanStyles) + clamp(annotations) + paragraphStarts.keys.retainAll { it in 0 until text.length } + } + + private fun clamp(ranges: MutableList>) { + val limit = text.length + for (index in ranges.indices.reversed()) { + val range = ranges[index] + when { + range.start >= limit -> ranges.removeAt(index) + range.end > limit -> ranges[index] = + AnnotatedString.Range(range.item, range.start, limit, range.tag) + } + } + } + + fun build(): AnnotatedString = buildAnnotatedString { + append(text.toString()) + paragraphStyles().forEach { addStyle(it.item, it.start, it.end) } + spanStyles.forEach { addStyle(it.item, it.start, it.end) } + annotations.forEach { addStringAnnotation(it.tag, it.item, it.start, it.end) } + } + + /** + * One style per paragraph, covering the whole output, carrying the indent recorded where the + * paragraph starts. The ranges have to be contiguous and complete, because their edges are the + * paragraph breaks that [endBlock] counted on for separation. + */ + private fun paragraphStyles(): List> { + if (text.isEmpty()) return emptyList() + val bounds = (paragraphStarts.keys + 0).sorted() + text.length + return bounds.zipWithNext { start, end -> + AnnotatedString.Range(ParagraphStyle(textIndent = indentFor(start)), start, end) + } + } + + private fun indentFor(start: Int): TextIndent? = paragraphStarts[start] + ?.takeIf { it != TextUnit.Unspecified } + ?.let { TextIndent(firstLine = it, restLine = it) } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownRenderer.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownRenderer.kt new file mode 100644 index 00000000000..967fd0031ea --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/internal/MarkdownRenderer.kt @@ -0,0 +1,632 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util.internal + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import io.getstream.chat.android.compose.ui.util.AnnotationTagBlockQuote +import io.getstream.chat.android.compose.ui.util.AnnotationTagLiteral +import io.getstream.chat.android.compose.ui.util.AnnotationTagUrl +import io.getstream.chat.android.compose.ui.util.MarkdownStyles +import io.getstream.log.taggedLogger +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode +import org.intellij.markdown.flavours.gfm.GFMElementTypes +import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor +import org.intellij.markdown.flavours.gfm.GFMTokenTypes +import org.intellij.markdown.parser.LinkMap +import org.intellij.markdown.parser.MarkdownParser + +/** + * Renders markdown as an [AnnotatedString]: inline constructs become spans, blocks are laid out + * with line breaks and indentation. A construct with no styled form falls back to something + * readable, an image to its alt text and a table to its source. + */ +internal class MarkdownRenderer(private val styles: MarkdownStyles) { + + private val logger by taggedLogger("Chat:MarkdownRenderer") + + fun render(text: String): AnnotatedString { + // The parser only recognises line feeds; a carriage return would reach the output. + val source = text.replace("\r\n", "\n").replace('\r', '\n') + // Runs during composition on text from other people, and nesting deep enough exhausts the + // stack while parsing. Failing here would take the message list down on every reopen. + return try { + renderOrThrow(source) + } catch (@Suppress("TooGenericExceptionCaught") error: Throwable) { + logger.e(error) { "[render] failed, falling back to plain text" } + AnnotatedString(source) + } + } + + private fun renderOrThrow(source: String): AnnotatedString { + val emitter = MarkdownEmitter() + val tree = MarkdownParser(GFMFlavourDescriptor()).buildMarkdownTreeFromString(source) + val links = LinkMap.buildLinkMap(tree, source) + Walker(source, styles, emitter, links).visitBlocks(tree.children) + emitter.trimTrailingNewlines() + val rendered = emitter.build() + // A message holding only link definitions renders nothing, so show what was typed. + return when { + rendered.text.isEmpty() && source.isNotEmpty() -> AnnotatedString(source) + else -> rendered + } + } +} + +// A visitor needs a member per construct, and markdown has more than the threshold allows. +@Suppress("TooManyFunctions") +private class Walker( + private val source: String, + private val styles: MarkdownStyles, + private val emitter: MarkdownEmitter, + private val links: LinkMap, +) { + + private var quoteDepth = 0 + + fun visitBlocks(nodes: List) { + // The breaks between two blocks are how the author spaced them, so carry that across + // rather than imposing a fixed separation per block type. + var breaks = 0 + var started = false + for (node in nodes) { + when (node.type) { + MarkdownTokenTypes.EOL -> if (started) breaks++ + MarkdownTokenTypes.WHITE_SPACE -> Unit + else -> { + if (started) emitter.endBlock(breaks.coerceIn(1, MaxBlockBreaks)) + breaks = 0 + started = true + visitBlock(node) + } + } + } + } + + private fun visitBlock(node: ASTNode) { + when (node.type) { + MarkdownElementTypes.PARAGRAPH -> visitInlineChildren(node) + + MarkdownElementTypes.ATX_1 -> heading(node, level = 1) + MarkdownElementTypes.ATX_2 -> heading(node, level = 2) + MarkdownElementTypes.ATX_3 -> heading(node, level = 3) + MarkdownElementTypes.ATX_4 -> heading(node, level = 4) + MarkdownElementTypes.ATX_5 -> heading(node, level = 5) + MarkdownElementTypes.ATX_6 -> heading(node, level = 6) + MarkdownElementTypes.SETEXT_1 -> heading(node, level = 1) + MarkdownElementTypes.SETEXT_2 -> heading(node, level = 2) + + MarkdownElementTypes.BLOCK_QUOTE -> blockQuote(node) + MarkdownElementTypes.UNORDERED_LIST, MarkdownElementTypes.ORDERED_LIST -> list(node, level = 1) + MarkdownElementTypes.CODE_FENCE -> codeBlock(node, contentType = MarkdownTokenTypes.CODE_FENCE_CONTENT) + MarkdownElementTypes.CODE_BLOCK -> + codeBlock(node, contentType = MarkdownTokenTypes.CODE_LINE, stripIndent = true) + + MarkdownTokenTypes.HORIZONTAL_RULE -> emitter.append(styles.thematicBreak) + + // No styled form, so the source stands in, but both are still blocks. + GFMElementTypes.TABLE, MarkdownElementTypes.HTML_BLOCK -> verbatimBlock(node) + + // A definition declares a reference target and renders nothing itself. + MarkdownElementTypes.LINK_DEFINITION -> Unit + + // endBlock drives the separation, so structural breaks are dropped. + MarkdownTokenTypes.EOL, MarkdownTokenTypes.WHITE_SPACE -> Unit + + else -> visitInlineNode(node) + } + } + + /** + * Emits a construct's source as written, less the quote markers its continuation lines carry, + * which the line prefix already stands for. + */ + private fun verbatimBlock(node: ASTNode) { + val start = emitter.length + node.text().toString().split('\n').forEachIndexed { index, line -> + if (index > 0) emitter.appendLineBreak() + // Appended rather than resolved, so an escape or a reference stays as it was written. + emitter.append(if (index > 0) QuoteMarkers.replace(line, "") else line) + } + emitter.addAnnotation(AnnotationTagLiteral, "", start) + } + + private fun heading(node: ASTNode, level: Int) { + val start = emitter.length + val content = node.children.filter { it.type in HeadingContentTypes } + if (content.isEmpty()) { + // Setext headings hold their text directly, ATX headings wrap it in a content node. + visitInlineChildren(node, skip = HeadingMarkerTypes) + } else { + // The content node keeps the spaces separating the text from either marker. + content.forEach { + visitInlineNodes( + it.children.dropWhile(ASTNode::isWhitespace).dropLastWhile(ASTNode::isWhitespace), + ) + } + } + emitter.addSpan(styles.heading(level), start) + } + + /** + * Sets the quote in from the margin and marks its span with the depth, leaving the rail to be + * drawn. A marker character could only land on a line this walker broke itself, so it would be + * missing from every line the layout wrapped. + */ + private fun blockQuote(node: ASTNode) { + val start = emitter.length + val depth = quoteDepth + 1 + quoteDepth = depth + emitter.withIndent(styles.blockQuoteIndent * depth) { + emitter.startParagraph() + visitBlocks(node.children.filter { it.type != MarkdownTokenTypes.BLOCK_QUOTE }) + emitter.trimTrailingNewlines() + } + quoteDepth = depth - 1 + emitter.addSpan(styles.blockQuote, start) + emitter.addAnnotation(AnnotationTagBlockQuote, depth.toString(), start) + } + + private fun list(node: ASTNode, level: Int) { + val items = node.children.filter { it.type == MarkdownElementTypes.LIST_ITEM } + val ordered = node.type == MarkdownElementTypes.ORDERED_LIST + // Numbered from the first marker on, so a list written entirely as "1." reads 1, 2, 3. + val firstNumber = items.firstNotNullOfOrNull(::orderedMarkerNumber) ?: 1 + emitter.withIndent(styles.listIndent * (level - 1)) { + items.forEachIndexed { index, item -> + // Every item is a paragraph of its own, which is what lets each one keep the + // indent of its own level once the layout wraps it. + emitter.startParagraph() + val marker = when { + ordered -> "${firstNumber + index}. " + else -> "$UnorderedListMarker " + } + listItem(item, level, marker) + } + } + } + + private fun orderedMarkerNumber(item: ASTNode): Int? = item.children + .firstOrNull { it.type == MarkdownTokenTypes.LIST_NUMBER } + ?.text() + ?.trimStart() + ?.takeWhile(Char::isDigit) + ?.toString() + ?.toIntOrNull() + + private fun listItem(node: ASTNode, level: Int, marker: String) { + emitter.append(marker) + + // Whatever comes first shares the marker's line, so no marker is left alone on one. + // Everything after starts its own line, indented under the item's text. + var markerLineTaken = false + for (child in node.children) { + when (child.type) { + // A checkbox belongs beside the marker, so it must not take the line. + GFMTokenTypes.CHECK_BOX -> emitter.append(child.text()) + + // Markers are replaced, and the breaks between an item's blocks are structural. + MarkdownTokenTypes.LIST_BULLET, + MarkdownTokenTypes.LIST_NUMBER, + MarkdownTokenTypes.EOL, + MarkdownTokenTypes.WHITE_SPACE, + -> Unit + + MarkdownElementTypes.PARAGRAPH -> { + if (markerLineTaken) continueItemLine() + markerLineTaken = true + // Items hold content in paragraphs; as blocks they would gain blank lines. + visitInlineChildren(child) + } + + // A nested list opens paragraphs of its own, which are the breaks as well. + MarkdownElementTypes.UNORDERED_LIST, MarkdownElementTypes.ORDERED_LIST -> { + markerLineTaken = true + list(child, level + 1) + } + + // Same for a quote, which also brings its own indent, so opening a line for it + // would leave an indented blank one behind. + MarkdownElementTypes.BLOCK_QUOTE -> { + markerLineTaken = true + blockQuote(child) + } + + else -> { + if (markerLineTaken) continueItemLine() + markerLineTaken = true + emitter.withLinePrefix(emitter.currentLinePrefix + BlockInItemIndent) { + visitBlock(child) + // Trim under the item's prefix, or its last marked line is left dangling. + emitter.trimTrailingNewlines() + } + } + } + } + // No break of its own: the next item opens a paragraph, and the block separator after the + // last one supplies the blank line. + emitter.trimTrailingNewlines() + } + + private fun endItemLine() { + emitter.trimTrailingNewlines() + emitter.endBlock(newlines = 1) + } + + private fun continueItemLine() { + endItemLine() + emitter.append(BlockInItemIndent) + } + + private fun codeBlock(node: ASTNode, contentType: IElementType, stripIndent: Boolean = false) { + val code = StringBuilder() + for (child in node.children) { + when (child.type) { + contentType -> code.append(child.text()) + MarkdownTokenTypes.EOL -> code.append('\n') + else -> Unit + } + } + val start = emitter.length + // Line by line, so a code block inside a quote keeps the quote's marker on each. + code.trim('\n').split('\n').forEachIndexed { index, line -> + if (index > 0) emitter.appendLineBreak() + emitter.append(if (stripIndent) line.stripCodeIndent() else line) + } + emitter.addSpan(styles.codeBlock, start) + emitter.addAnnotation(AnnotationTagLiteral, "", start) + } + + private fun visitInlineChildren(node: ASTNode, skip: Set = emptySet()) { + visitInlineNodes(node.children.filter { it.type !in skip }) + } + + private fun visitInlineNodes(nodes: List) { + // A quote marks every line, so a continuation line carries one inside the paragraph. + var afterQuoteMarker = false + var afterHardBreak = false + nodes.forEachIndexed { index, node -> + val bracketsEmailAutolink = node.type == MarkdownTokenTypes.LT && + nodes.getOrNull(index + 1)?.type == MarkdownTokenTypes.EMAIL_AUTOLINK || + node.type == MarkdownTokenTypes.GT && + nodes.getOrNull(index - 1)?.type == MarkdownTokenTypes.EMAIL_AUTOLINK + when { + node.type == MarkdownTokenTypes.BLOCK_QUOTE -> afterQuoteMarker = true + afterQuoteMarker && node.type == MarkdownTokenTypes.WHITE_SPACE -> afterQuoteMarker = false + // A hard break is a marker plus the feed it sits on; only the marker breaks. + afterHardBreak && node.type == MarkdownTokenTypes.EOL -> afterHardBreak = false + // An email autolink is a bare token between brackets, unlike a URL autolink. + bracketsEmailAutolink -> afterQuoteMarker = false + else -> { + afterQuoteMarker = false + afterHardBreak = node.isHardBreak(source) + visitInlineNode(node) + } + } + } + } + + private fun visitInlineNode(node: ASTNode) { + when (node.type) { + MarkdownElementTypes.EMPH -> styled(ItalicSpan) { + visitInlineChildren(node, skip = EmphasisMarkerTypes) + } + + MarkdownElementTypes.STRONG -> styled(BoldSpan) { + visitInlineChildren(node, skip = EmphasisMarkerTypes) + } + + GFMElementTypes.STRIKETHROUGH -> styled(StrikethroughSpan) { + visitInlineChildren(node, skip = EmphasisMarkerTypes) + } + + MarkdownElementTypes.CODE_SPAN -> literal(styles.codeSpan) { + // The specification turns a line ending in a code span into a space. + val content = node.children + .dropWhile { it.type == MarkdownTokenTypes.BACKTICK } + .dropLastWhile { it.type == MarkdownTokenTypes.BACKTICK } + content.forEachIndexed { index, child -> + val opensQuotedLine = + content.getOrNull(index - 1)?.type == MarkdownTokenTypes.BLOCK_QUOTE + when { + child.type == MarkdownTokenTypes.BLOCK_QUOTE -> Unit + opensQuotedLine && child.type == MarkdownTokenTypes.WHITE_SPACE -> Unit + child.type == MarkdownTokenTypes.EOL -> emitter.append(" ") + else -> emitter.append(child.text()) + } + } + } + + MarkdownElementTypes.INLINE_LINK -> inlineLink(node) + + MarkdownElementTypes.FULL_REFERENCE_LINK, + MarkdownElementTypes.SHORT_REFERENCE_LINK, + -> referenceLink(node) + + // An image cannot be drawn, so its alt text stands in, as the specification says. + MarkdownElementTypes.IMAGE -> imageAltText(node) + + // The brackets are syntax; the entity pass linkifies the URL like any bare one. + MarkdownElementTypes.AUTOLINK -> visitInlineChildren(node, skip = AutolinkMarkerTypes) + MarkdownTokenTypes.EMAIL_AUTOLINK, MarkdownTokenTypes.AUTOLINK -> emitter.append(node.text()) + + MarkdownTokenTypes.HARD_LINE_BREAK, MarkdownTokenTypes.EOL -> emitter.appendLineBreak() + + MarkdownTokenTypes.HTML_TAG -> when { + node.isHardBreak(source) -> emitter.appendLineBreak() + else -> emitter.appendText(node.text()) + } + + else -> + if (node.children.isEmpty()) { + emitter.appendText(node.text()) + } else { + visitInlineChildren(node) + } + } + } + + /** + * Emits what a link, reference or image shows, annotated with [destination] when there is one + * worth opening. Showing nothing falls back to the source, so nothing vanishes. + */ + private fun linkLike(node: ASTNode, label: ASTNode?, destination: String?) { + if (label == null) { + emitter.appendText(node.text()) + return + } + val start = emitter.length + visitInlineChildren(label, skip = LinkLabelMarkerTypes) + if (emitter.length == start) { + emitter.appendText(node.text()) + return + } + destination?.resolveMarkdownText()?.toOpenableUrl()?.let { url -> + emitter.addAnnotation(AnnotationTagUrl, url, start) + } + } + + private fun inlineLink(node: ASTNode) { + val destination = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_DESTINATION } + when (destination) { + null -> emitter.appendText(node.text()) + else -> linkLike( + node = node, + label = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT }, + destination = destination.text().toString(), + ) + } + } + + /** Resolves `[text][label]` and `[label]` against the document's link definitions. */ + private fun referenceLink(node: ASTNode) { + val label = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_LABEL } + val destination = label + ?.let { links.getLinkInfo(LinkMap.normalizeLabel(it.text())) } + ?.destination + when (destination) { + // With no definition to resolve against, the reference reads as it was written. + null -> emitter.appendText(node.text()) + else -> linkLike( + node = node, + // A full reference shows its own text; a short one shows the label it was written as. + label = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT } ?: label, + destination = destination.toString(), + ) + } + } + + private fun imageAltText(node: ASTNode) { + val link = node.children.firstOrNull { it.type in ImageLinkTypes } ?: node + linkLike( + node = node, + label = link.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT } + ?: link.children.firstOrNull { it.type == MarkdownElementTypes.LINK_LABEL }, + destination = null, + ) + } + + private inline fun styled(style: SpanStyle, content: () -> Unit) { + val start = emitter.length + content() + emitter.addSpan(style, start) + } + + /** Styles [content] and marks it literal, as code is. */ + private inline fun literal(style: SpanStyle, content: () -> Unit) { + val start = emitter.length + content() + emitter.addSpan(style, start) + emitter.addAnnotation(AnnotationTagLiteral, "", start) + } + + private fun ASTNode.text(): CharSequence = getTextInNode(source) +} + +/** Appends source text with its escapes and character references resolved. */ +private fun MarkdownEmitter.appendText(value: CharSequence) { + value.toString().resolveMarkdownText().split('\n').forEachIndexed { index, line -> + if (index > 0) appendLineBreak() + append(line) + } +} + +/** + * Resolves the backslash escapes and character references the parser leaves in place, in a link's + * destination as much as in the text. + */ +private fun String.resolveMarkdownText(): String = buildString { + val source = this@resolveMarkdownText + var index = 0 + while (index < source.length) { + val char = source[index] + val next = source.getOrNull(index + 1) + val reference = if (char == '&') source.characterReferenceAt(index) else null + when { + char == '\\' && next != null && next in EscapablePunctuation -> { + append(next) + index += 2 + } + + reference != null -> { + append(reference.first) + index += reference.second + } + + else -> { + append(char) + index++ + } + } + } +} + +/** + * Decodes the character reference at [start] into its text and length, or null when what follows + * the `&` is not one. Only the numeric forms and the named ones below are decoded. + */ +private fun CharSequence.characterReferenceAt(start: Int): Pair? { + // Bounded, so that a message full of ampersands does not scan its own tail for each one. + val limit = minOf(length, start + 1 + MaxCharacterReferenceLength) + val semicolon = (start + 1 until limit).firstOrNull { this[it] == ';' } ?: return null + val body = subSequence(start + 1, semicolon).toString() + if (body.isEmpty()) return null + val length = semicolon - start + 1 + NamedCharacterReferences[body]?.let { return it to length } + if (!body.startsWith("#")) return null + val codePoint = when { + body.startsWith("#x", ignoreCase = true) -> body.drop(2).toIntOrNull(radix = 16) + else -> body.drop(1).toIntOrNull() + } ?: return null + // An invalid code point, a surrogate included, becomes the replacement character. + val invalid = codePoint <= 0 || + codePoint > Character.MAX_CODE_POINT || + codePoint in MinSurrogate..MaxSurrogate + if (invalid) return ReplacementCharacter to length + return String(Character.toChars(codePoint)) to length +} + +/** + * Turns a link destination into something openable, or null when it is not. A fragment or a path + * only means something inside a document, and a URL invented from one fails when tapped. + * + * A dotted destination with no path is taken for a host, so `getstream.io` links. A file name like + * `readme.md` cannot be told apart and links too, as it does in the View-based and iOS kits. + */ +private fun String.toOpenableUrl(): String? { + val destination = removeSurrounding("<", ">").trim() + return when { + destination.isEmpty() || destination.any(Char::isWhitespace) -> null + destination.hasOpenableScheme() -> destination.lowercaseScheme() + destination.contains('@') && !destination.contains('/') -> "mailto:$destination" + // Also reached by a host carrying a port, which reads as a scheme above. HostPattern is + // anchored on a dotted host, so a hostile scheme cannot match here. + HostPattern.containsMatchIn(destination) -> "https://$destination" + else -> null + } +} + +/** + * A tapped link is handed to the system, so only a scheme a message has business carrying is + * annotated. Otherwise text reading as ordinary prose could open a `javascript:` or `intent://` + * target, or deep link into the host app. + */ +private fun String.hasOpenableScheme(): Boolean = OpenableSchemes.any { scheme -> + // A destination that is only a scheme has nothing to open, so it is not a link. + startsWith(scheme, ignoreCase = true) && length > scheme.length +} + +/** Android matches an intent filter's scheme case-sensitively, so it has to be lowercase. */ +private fun String.lowercaseScheme(): String { + val separator = indexOf(':') + return substring(0, separator).lowercase() + substring(separator) +} + +/** + * Both spellings of a hard break: the marker left by trailing spaces or a backslash, and the tag. + * Either absorbs the line feed it sits on, so ending a line with one breaks it once. + */ +private fun ASTNode.isHardBreak(source: String): Boolean = when (type) { + MarkdownTokenTypes.HARD_LINE_BREAK -> true + MarkdownTokenTypes.HTML_TAG -> getTextInNode(source).toString().isLineBreakTag() + else -> false +} + +private fun String.isLineBreakTag(): Boolean = LineBreakTagPattern.matches(trim()) + +private fun ASTNode.isWhitespace(): Boolean = type == MarkdownTokenTypes.WHITE_SPACE + +private val ItalicSpan = SpanStyle(fontStyle = FontStyle.Italic) +private val BoldSpan = SpanStyle(fontWeight = FontWeight.Bold) +private val StrikethroughSpan = SpanStyle(textDecoration = TextDecoration.LineThrough) + +/** A block sitting inside a list item is set in from the item's text. */ +private const val BlockInItemIndent = " " + +private const val UnorderedListMarker = "•" + +/** Blocks are separated by at most a blank line, however many breaks the source holds. */ +private const val MaxBlockBreaks = 2 + +private val OpenableSchemes = listOf("http://", "https://", "mailto:", "tel:") +private val HostPattern = Regex("^[\\w\\-]+(\\.[\\w\\-]+)+") +private val LineBreakTagPattern = Regex("", RegexOption.IGNORE_CASE) +private const val EscapablePunctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" +private const val MaxCharacterReferenceLength = 32 +private const val ReplacementCharacter = "\uFFFD" +private const val MinSurrogate = 0xD800 +private const val MaxSurrogate = 0xDFFF + +private val NamedCharacterReferences = mapOf( + "amp" to "&", + "lt" to "<", + "gt" to ">", + "quot" to "\"", + "apos" to "'", + "nbsp" to "\u00A0", +) + +private val HeadingContentTypes = setOf(MarkdownTokenTypes.ATX_CONTENT, MarkdownTokenTypes.SETEXT_CONTENT) +private val HeadingMarkerTypes = setOf( + MarkdownTokenTypes.ATX_HEADER, + MarkdownTokenTypes.SETEXT_1, + MarkdownTokenTypes.SETEXT_2, +) +private val EmphasisMarkerTypes = setOf(MarkdownTokenTypes.EMPH, GFMTokenTypes.TILDE) +private val LinkLabelMarkerTypes = setOf(MarkdownTokenTypes.LBRACKET, MarkdownTokenTypes.RBRACKET) +private val AutolinkMarkerTypes = setOf(MarkdownTokenTypes.LT, MarkdownTokenTypes.GT) +private val QuoteMarkers = Regex("^(?:> ?)+") +private val ImageLinkTypes = setOf( + MarkdownElementTypes.INLINE_LINK, + MarkdownElementTypes.FULL_REFERENCE_LINK, + MarkdownElementTypes.SHORT_REFERENCE_LINK, +) + +/** CommonMark strips the indentation that declared an indented code block, in either spelling. */ +private fun String.stripCodeIndent(): String = when { + startsWith(IndentedCodeSpaces) -> removePrefix(IndentedCodeSpaces) + else -> removePrefix("\t") +} + +private const val IndentedCodeSpaces = " " diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatterTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatterTest.kt new file mode 100644 index 00000000000..7b84da80646 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownMessageTextFormatterTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import io.getstream.chat.android.compose.ui.theme.StreamTypography +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.User +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test + +internal class MarkdownMessageTextFormatterTest { + + private val formatter = MarkdownMessageTextFormatter( + autoTranslationEnabled = false, + typography = StreamTypography.defaultTypography(), + styles = MarkdownStyles( + headings = listOf(30, 26, 22, 18, 16, 14).map { SpanStyle(fontSize = it.sp) }, + codeSpan = SpanStyle(fontFamily = FontFamily.Monospace), + codeBlock = SpanStyle(fontFamily = FontFamily.Monospace), + blockQuote = SpanStyle(color = Color.Gray), + ), + textStyle = { _, _ -> TextStyle(color = Color.Black) }, + linkStyle = { TextStyle(color = Color.Blue) }, + mentionColor = { Color.Unspecified }, + builder = null, + ) + + private val currentUser = User(id = "me") + + @Test + fun `highlights a mention alongside markdown`() { + val mentioned = User(id = "u1", name = "Martin") + val message = message(text = "**hey** @Martin", mentionedUsers = listOf(mentioned)) + + val result = formatter.format(message, currentUser) + + result.text shouldBeEqualTo "hey @Martin" + result.annotation(UserMentionTag, "@Martin") shouldBeEqualTo "Martin" + result.spanAt("hey")?.fontWeight shouldBeEqualTo FontWeight.Bold + } + + @Test + fun `highlights a mention whose name is itself emphasised`() { + val mentioned = User(id = "u1", name = "Martin") + val message = message(text = "hey @**Martin**", mentionedUsers = listOf(mentioned)) + + val result = formatter.format(message, currentUser) + + result.text shouldBeEqualTo "hey @Martin" + result.annotation(UserMentionTag, "@Martin") shouldBeEqualTo "Martin" + result.spanAt("Martin")?.fontWeight shouldBeEqualTo FontWeight.Bold + } + + @Test + fun `linkifies a bare url after markdown has been rendered`() { + val result = formatter.format(message(text = "see *this*: https://getstream.io"), currentUser) + + result.text shouldBeEqualTo "see this: https://getstream.io" + result.annotation(AnnotationTagUrl, "https://getstream.io") shouldBeEqualTo "https://getstream.io" + } + + @Test + fun `keeps the markdown destination when the link label looks like a url`() { + val message = message(text = "[https://text-link.com](https://real-link.com)") + + val result = formatter.format(message, currentUser) + + result.text shouldBeEqualTo "https://text-link.com" + result.annotation(AnnotationTagUrl, "https://text-link.com") shouldBeEqualTo "https://real-link.com" + } + + @Test + fun `styles a markdown link like a detected one`() { + val result = formatter.format(message(text = "see [the docs](https://getstream.io)"), currentUser) + + result.spanAt("the docs")?.color shouldBeEqualTo Color.Blue + } + + @Test + fun `linkifies an autolink once its brackets are gone`() { + val result = formatter.format(message(text = "visit now"), currentUser) + + result.text shouldBeEqualTo "visit https://getstream.io now" + result.annotation(AnnotationTagUrl, "https://getstream.io") shouldBeEqualTo "https://getstream.io" + } + + @Test + fun `does not linkify a url inside a code span`() { + val result = formatter.format(message(text = "run `curl https://getstream.io` now"), currentUser) + + result.text shouldBeEqualTo "run curl https://getstream.io now" + result.annotation(AnnotationTagUrl, "https://getstream.io") shouldBeEqualTo null + } + + @Test + fun `does not highlight a mention inside a code span`() { + val mentioned = User(id = "u1", name = "Martin") + val message = message(text = "see `@Martin` here", mentionedUsers = listOf(mentioned)) + + val result = formatter.format(message, currentUser) + + result.text shouldBeEqualTo "see @Martin here" + result.annotation(UserMentionTag, "@Martin") shouldBeEqualTo null + } + + @Test + fun `keeps the markdown styling underneath a link`() { + // The link contributes colour only, so markdown styling underneath survives. + val bold = formatter.format(message(text = "**[link](https://x.com)**"), currentUser) + bold.spanAt("link")?.fontWeight shouldBeEqualTo FontWeight.Bold + bold.spanAt("link")?.color shouldBeEqualTo Color.Blue + + val heading = formatter.format(message(text = "# [link](https://x.com)"), currentUser) + heading.spanAt("link")?.fontSize shouldBeEqualTo 30.sp + heading.spanAt("link")?.color shouldBeEqualTo Color.Blue + } + + @Test + fun `keeps the markdown styling underneath a detected link`() { + // The entity pass styles links it finds itself, and must narrow them the same way. + val result = formatter.format(message(text = "# See https://getstream.io"), currentUser) + + result.spanAt("https://getstream.io")?.fontSize shouldBeEqualTo 30.sp + result.spanAt("https://getstream.io")?.color shouldBeEqualTo Color.Blue + } + + @Test + fun `leaves plain text with line breaks untouched`() { + val text = "first line\nsecond line" + + formatter.format(message(text = text), currentUser).text shouldBeEqualTo text + } + + @Test + fun `applies the base text color to the whole message`() { + val result = formatter.format(message(text = "# Title\nbody"), currentUser) + + result.spanAt("body")?.color shouldBeEqualTo Color.Black + } + + @Test + fun `renders the translation when auto translation is on`() { + val translating = MarkdownMessageTextFormatter( + autoTranslationEnabled = true, + typography = StreamTypography.defaultTypography(), + styles = MarkdownStyles( + headings = List(6) { SpanStyle() }, + codeSpan = SpanStyle(), + codeBlock = SpanStyle(), + blockQuote = SpanStyle(), + ), + textStyle = { _, _ -> TextStyle(color = Color.Black) }, + linkStyle = { TextStyle(color = Color.Blue) }, + mentionColor = { Color.Unspecified }, + builder = null, + ) + val message = message(text = "**hello**").copy(i18n = mapOf("it_text" to "**ciao**")) + + val result = translating.format(message, User(id = "me", language = "it")) + + result.text shouldBeEqualTo "ciao" + result.spanAt("ciao")?.fontWeight shouldBeEqualTo FontWeight.Bold + } + + private fun message(text: String, mentionedUsers: List = emptyList()) = Message( + id = "message-id", + cid = "messaging:channel-id", + text = text, + user = User(id = "other"), + mentionedUsers = mentionedUsers, + ) +} + +/** Mirrors the tag the Compose kit annotates user mentions with. */ +private const val UserMentionTag = "MENTION" + +private fun AnnotatedString.annotation(tag: String, substring: String): String? { + val start = text.indexOf(substring) + if (start < 0) return null + return getStringAnnotations(tag, start, start + substring.length).firstOrNull()?.item +} + +private fun AnnotatedString.spanAt(substring: String): SpanStyle? { + val start = text.indexOf(substring) + if (start < 0) return null + val covering = spanStyles.filter { it.start <= start && it.end >= start + substring.length } + if (covering.isEmpty()) return null + return covering.map { it.item }.reduce { merged, style -> merged.merge(style) } +} diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRendererTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRendererTest.kt new file mode 100644 index 00000000000..d86a80168f4 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRendererTest.kt @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextIndent +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import io.getstream.chat.android.compose.ui.util.internal.MarkdownRenderer +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +internal class MarkdownRendererTest { + + private val renderer = MarkdownRenderer(TestStyles) + + @ParameterizedTest + @MethodSource("renderedTextArguments") + fun `renders the text markdown produces`(source: String, expected: String) { + // Read back with the paragraph breaks materialised, since a list expresses its line + // breaks as paragraphs rather than as line feeds. + renderer.render(source).textWithParagraphBreaks() shouldBeEqualTo expected + } + + @Test + fun `styles bold text`() { + val result = renderer.render("say **that** now") + + result.text shouldBeEqualTo "say that now" + result.spanAt("that")?.fontWeight shouldBeEqualTo FontWeight.Bold + } + + @Test + fun `styles italic text`() { + val result = renderer.render("say *that* now") + + result.text shouldBeEqualTo "say that now" + result.spanAt("that")?.fontStyle shouldBeEqualTo FontStyle.Italic + } + + @Test + fun `styles bold italic text`() { + val result = renderer.render("say ***that*** now") + + result.text shouldBeEqualTo "say that now" + result.spanAt("that")?.fontWeight shouldBeEqualTo FontWeight.Bold + result.spanAt("that")?.fontStyle shouldBeEqualTo FontStyle.Italic + } + + @Test + fun `styles strikethrough text`() { + val result = renderer.render("say ~~that~~ now") + + result.text shouldBeEqualTo "say that now" + result.spanAt("that")?.textDecoration shouldBeEqualTo TextDecoration.LineThrough + } + + @Test + fun `styles an inline code span`() { + val result = renderer.render("call `render()` first") + + result.text shouldBeEqualTo "call render() first" + result.spanAt("render()")?.fontFamily shouldBeEqualTo FontFamily.Monospace + } + + @Test + fun `styles a fenced code block and keeps its line breaks`() { + val result = renderer.render("before\n\n```kotlin\nval a = 1\n\nval b = 2\n```") + + result.text shouldBeEqualTo "before\nval a = 1\n\nval b = 2" + result.spanAt("val a = 1")?.fontFamily shouldBeEqualTo FontFamily.Monospace + } + + @Test + fun `styles a heading per level`() { + val levels = (1..6).map { level -> + val marker = "#".repeat(level) + renderer.render("$marker Title").spanAt("Title")?.fontSize + } + + levels shouldBeEqualTo listOf(30.sp, 26.sp, 22.sp, 18.sp, 16.sp, 14.sp) + } + + @Test + fun `indents nested list items`() { + val result = renderer.render("- one\n - two\n - three") + + result.textWithParagraphBreaks() shouldBeEqualTo "• one\n• two\n• three" + // One paragraph per item, indented by its level, so a wrapped line stays under its marker. + result.paragraphStyles.map { it.item.textIndent } shouldBeEqualTo listOf( + TextIndent(firstLine = 0.em, restLine = 0.em), + TextIndent(firstLine = 3.em, restLine = 3.em), + TextIndent(firstLine = 6.em, restLine = 6.em), + ) + } + + @Test + fun `indents a quote and marks its depth, leaving its rail to be drawn`() { + val result = renderer.render("> quoted") + + result.paragraphStyles.single().item.textIndent shouldBeEqualTo TextIndent(5.em, 5.em) + result.quoteDepths() shouldBeEqualTo listOf("1") + } + + @Test + fun `stacks the indent and the depth of a nested quote`() { + val result = renderer.render("> outer\n> > inner") + + result.paragraphStyles.map { it.item.textIndent } shouldBeEqualTo listOf( + TextIndent(5.em, 5.em), + TextIndent(10.em, 10.em), + ) + result.quoteDepths() shouldBeEqualTo listOf("1", "2") + } + + @Test + fun `separates two blocks with a paragraph break rather than a second line feed`() { + val result = renderer.render("- one\n\nafter") + + result.text shouldBeEqualTo "• one\nafter" + result.paragraphStyles.map { it.start to it.end } shouldBeEqualTo listOf(0 to 6, 6 to 11) + } + + @Test + fun `marks a quote's whole span, so its rail runs through the gap between its paragraphs`() { + val result = renderer.render("> one\n>\n> two") + + val quote = result.getStringAnnotations(AnnotationTagBlockQuote, 0, result.length).single() + quote.start to quote.end shouldBeEqualTo (0 to result.length) + } + + @Test + fun `keeps the ordinals of an ordered list`() { + renderer.render("1. one\n1. two\n1. three") + .textWithParagraphBreaks() shouldBeEqualTo "1. one\n2. two\n3. three" + } + + @Test + fun `annotates an inline link with its destination`() { + val result = renderer.render("see [the docs](https://getstream.io/chat) now") + + result.text shouldBeEqualTo "see the docs now" + result.urlAt("the docs") shouldBeEqualTo "https://getstream.io/chat" + } + + @Test + fun `makes a schemeless link destination absolute`() { + val result = renderer.render("[link](getstream.io)") + + result.urlAt("link") shouldBeEqualTo "https://getstream.io" + } + + @Test + fun `keeps the destination of a link whose label is itself a url`() { + val result = renderer.render("[https://text-link.com](https://real-link.com)") + + result.text shouldBeEqualTo "https://text-link.com" + result.urlAt("https://text-link.com") shouldBeEqualTo "https://real-link.com" + } + + @Test + fun `renders an image as its alt text, since images cannot be drawn`() { + renderer.render("![alt text](https://example.com/a.png)").text shouldBeEqualTo "alt text" + } + + @Test + fun `renders a reference image as its alt text too`() { + renderer.render("![alt][d]\n\n[d]: https://x.com/a.png").text shouldBeEqualTo "alt" + } + + @ParameterizedTest + @MethodSource("unopenableDestinations") + fun `does not annotate a destination that cannot be opened`(destination: String) { + val result = renderer.render("[label]($destination)") + + result.text shouldBeEqualTo "label" + result.urlAt("label") shouldBeEqualTo null + } + + @ParameterizedTest + @MethodSource("hostileSchemes") + fun `does not annotate a scheme a message has no business carrying`(destination: String) { + val result = renderer.render("[tap]($destination)") + + result.text shouldBeEqualTo "tap" + result.urlAt("tap") shouldBeEqualTo null + } + + @ParameterizedTest + @MethodSource("openableSchemes") + fun `annotates a scheme a message may carry`(destination: String) { + renderer.render("[tap]($destination)").urlAt("tap") shouldBeEqualTo destination + } + + @Test + fun `links a destination carrying a port`() { + renderer.render("[label](example.com:8080)").urlAt("label") shouldBeEqualTo + "https://example.com:8080" + } + + @Test + fun `resolves escapes and references inside a destination`() { + renderer.render("[x](https://a.com?a=1&b=2)").urlAt("x") shouldBeEqualTo + "https://a.com?a=1&b=2" + renderer.render("[x](https://a.com/a\\_b)").urlAt("x") shouldBeEqualTo "https://a.com/a_b" + } + + @Test + fun `falls back to plain text when a document cannot be rendered`() { + // Nesting this deep exhausts the stack while parsing. + val source = ">".repeat(2000) + " x" + + renderer.render(source).text shouldBeEqualTo source + } + + @Test + fun `resolves a full reference link`() { + val result = renderer.render("see [the docs][d] now\n\n[d]: https://getstream.io") + + result.text shouldBeEqualTo "see the docs now" + result.urlAt("the docs") shouldBeEqualTo "https://getstream.io" + } + + @Test + fun `resolves a short reference link`() { + val result = renderer.render("see [d] now\n\n[d]: getstream.io") + + result.text shouldBeEqualTo "see d now" + result.urlAt("d") shouldBeEqualTo "https://getstream.io" + } + + @Test + fun `leaves a reference link with no definition as written`() { + renderer.render("see [the docs][nope] now").text shouldBeEqualTo "see [the docs][nope] now" + } + + @Test + fun `does not hang on pathological emphasis markers`() { + // Regression input taken from the iOS SDK, which once hung on it. + val source = "**~*~~~*~*~**~*~* h e a r d ***~*~*~**~*~~~*" + + renderer.render(source).text.isNotEmpty() shouldBeEqualTo true + } + + // JUnit resolves these by name for @MethodSource, so nothing references them in code. + companion object { + + @JvmStatic + @Suppress("unused") + fun unopenableDestinations(): List = listOf("#section", "/docs/page", "foo bar") + + @JvmStatic + @Suppress("unused") + fun hostileSchemes(): List = listOf( + "javascript:alert(1)", + "intent://scan/#Intent;scheme=zxing;end", + "file:///data/data/x", + "myapp://reset?token=1", + "unknown-scheme:whatever", + ) + + @JvmStatic + @Suppress("unused") + fun openableSchemes(): List = listOf( + "http://x.com", + "https://x.com", + "mailto:a@b.com", + "tel:+123", + ) + + @JvmStatic + @Suppress("unused") + fun renderedTextArguments(): List = textArguments() + blockArguments() + + private fun textArguments(): List = listOf( + Arguments.of("plain text", "plain text"), + // Blocks are separated by the breaks the author wrote, no more and no fewer. + Arguments.of("Shopping list:\n- milk\n- eggs", "Shopping list:\n• milk\n• eggs"), + Arguments.of("intro\n# Heading", "intro\nHeading"), + Arguments.of("# Heading\n\nbody", "Heading\n\nbody"), + // A block inside a list item or quote keeps its own indentation; only the quote + // markers of a continuation line go. + Arguments.of(">
\n> x\n>
", "
\n x\n
"), + // A soft break stays a line break, or enabling markdown would join lines. + Arguments.of("first\nsecond", "first\nsecond"), + Arguments.of("first\n\nsecond", "first\n\nsecond"), + // Hard breaks, in their three spellings. + Arguments.of("first \nsecond", "first\nsecond"), + Arguments.of("first\\\nsecond", "first\nsecond"), + Arguments.of("first
second", "first\nsecond"), + // A tag ending a line absorbs that line's feed, exactly as trailing spaces do. + Arguments.of("first
\nsecond", "first\nsecond"), + Arguments.of("# Title\nbody", "Title\nbody"), + Arguments.of("> quoted", "quoted"), + // A soft break inside a quote is one block, and every line of it is marked. + Arguments.of("> quoted\n> continued", "quoted\ncontinued"), + // A blank line ends a quote, so this is two of them, kept apart. + Arguments.of("> first\n\n> second", "first\n\nsecond"), + // A quote holding two paragraphs marks the blank line between them too. + Arguments.of("> one\n>\n> two", "one\n\ntwo"), + // A hard break inside a quote opens exactly one new marked line. + Arguments.of("> one \n> two", "one\ntwo"), + // Nesting stacks the markers. + Arguments.of("> outer\n> > inner", "outer\ninner"), + Arguments.of("- one\n- two", "• one\n• two"), + Arguments.of("---\nafter", "***\nafter"), + // Escapes are resolved, and a backslash that escapes nothing is left alone. + Arguments.of("5 \\* 3", "5 * 3"), + Arguments.of("C:\\path\\to", "C:\\path\\to"), + // Autolink brackets are syntax; the entity pass linkifies the bare URL. + Arguments.of("visit now", "visit https://getstream.io now"), + Arguments.of("mail now", "mail a@b.com now"), + // Character references are resolved, named and numeric alike. + Arguments.of("a & b <c>", "a & b "), + Arguments.of("a & b & c", "a & b & c"), + // Anything that only looks like one is left as typed. + Arguments.of("a ¬real; b", "a ¬real; b"), + // Code content is literal, so a reference inside it stays as written. + Arguments.of("`a & b`", "a & b"), + ) + + private fun blockArguments(): List = listOf( + // An item whose only content is a block keeps it on the marker's line. + Arguments.of("- # H", "• H"), + Arguments.of("- > quoted", "• \nquoted"), + Arguments.of("- ```\n x\n ```", "• x"), + Arguments.of("- - a", "• \n• a"), + Arguments.of("1. # H\n1. next", "1. H\n2. next"), + // A second block starts its own line, indented under the item's text. + Arguments.of("- item\n\n # H\n- next", "• item\n H\n• next"), + Arguments.of("- item\n\n > q\n- next", "• item\nq\n• next"), + // Content following a nested list stays indented, and the next item still gets a line. + Arguments.of("- a\n - b\n\n more\n- c", "• a\n• b\n more\n• c"), + // A code block inside a quote keeps the marker on every line. + Arguments.of("> ```\n> one\n> two\n> ```", "one\ntwo"), + // An indented code block loses the indentation that declared it. + Arguments.of(" one\n two", "one\ntwo"), + // A table is still a block, so what follows it starts on a new line. + Arguments.of("| a | b |\n| - | - |\n\nafter", "| a | b |\n| - | - |\n\nafter"), + // Carriage returns never survive into the output. + Arguments.of("a\r\n\r\nb", "a\n\nb"), + Arguments.of("a\r\nb", "a\nb"), + // A heading keeps neither the space before its text nor the one after it. + Arguments.of("# H \nnext", "H\nnext"), + Arguments.of("# H #\nnext", "H\nnext"), + // Two breaks in the source stay two breaks. + Arguments.of("a

b", "a\n\nb"), + // An HTML block is a block, so what follows it starts on a new line. + Arguments.of("
x
\n\nafter", "
x
\n\nafter"), + // A document that renders to nothing falls back to what was typed. + Arguments.of("[d]: https://getstream.io", "[d]: https://getstream.io"), + // A checkbox belongs beside the marker rather than pushing the item onto a new line. + Arguments.of("- [x] done", "• [x] done"), + Arguments.of("- [ ] todo", "• [ ] todo"), + // Tab indentation declares an indented code block just as four spaces do. + Arguments.of("\tone\n\ttwo", "one\ntwo"), + // Every line of a block inside a list item is indented, not only the first. + Arguments.of("- item\n\n one\n two", "• item\n one\n two"), + // The specification turns a line ending inside a code span into a space. + Arguments.of("> `a\n> b`", "a b"), + // A reference to an invalid code point becomes the replacement character. + Arguments.of("a � b", "a \uFFFD b"), + Arguments.of("a � b", "a \uFFFD b"), + // A link or image with an empty label keeps its source, rather than disappearing. + Arguments.of("see [](https://x.com) here", "see [](https://x.com) here"), + Arguments.of("![](https://x.com/a.png)", "![](https://x.com/a.png)"), + // Only the delimiter runs are syntax, so a backtick between them is content. + Arguments.of("``a `b` c``", "a `b` c"), + // Verbatim source drops the quote markers of the lines it continues on. + Arguments.of(">
\n> x\n>
", "
\nx\n
"), + Arguments.of("> | a |\n> | - |", "| a |\n| - |"), + // A message of only whitespace is still text the sender typed. + Arguments.of(" ", " "), + // Unsupported constructs keep their source text so nothing is lost. + Arguments.of("| a | b |\n| --- | --- |\n| 1 | 2 |", "| a | b |\n| --- | --- |\n| 1 | 2 |"), + ) + } +} + +/** Sorted, because a quote is marked once its content is walked, so the innermost lands first. */ +private fun AnnotatedString.quoteDepths(): List = + getStringAnnotations(AnnotationTagBlockQuote, 0, length).map { it.item }.sorted() + +private val TestStyles = MarkdownStyles( + headings = listOf(30, 26, 22, 18, 16, 14).map { SpanStyle(fontSize = it.sp) }, + codeSpan = SpanStyle(fontFamily = FontFamily.Monospace), + codeBlock = SpanStyle(fontFamily = FontFamily.Monospace), + blockQuote = SpanStyle(color = Color.Gray), + listIndent = 3.em, + blockQuoteIndent = 5.em, + thematicBreak = "***", +) + +/** The merged span covering [substring], or null when it carries no style of its own. */ +private fun AnnotatedString.spanAt(substring: String): SpanStyle? { + val start = text.indexOf(substring) + if (start < 0) return null + val covering = spanStyles.filter { it.start <= start && it.end >= start + substring.length } + if (covering.isEmpty()) return null + return covering.map { it.item }.reduce { merged, style -> merged.merge(style) } +} + +private fun AnnotatedString.urlAt(substring: String): String? { + val start = text.indexOf(substring) + if (start < 0) return null + return getStringAnnotations(AnnotationTagUrl, start, start + substring.length).firstOrNull()?.item +} diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRtlSnapshotTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRtlSnapshotTest.kt new file mode 100644 index 00000000000..999e9e39637 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownRtlSnapshotTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import io.getstream.chat.android.compose.ui.PaparazziComposeTest +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.User +import org.junit.Rule +import org.junit.Test + +/** + * Renders markdown in a right-to-left layout, where a quote's rail has to be mirrored to the side + * its text is set in from. Lists need nothing: their indent and their marker are both laid out + * from the start edge already. + * + * Only right-to-left content, and the layout direction is provided rather than left to the + * device's locale. Paparazzi resolves an unspecified text direction from the layout alone, where a + * device resolves it from the content of each paragraph, so a message mixing the two directions + * renders here in a way no device would show and cannot be covered by a snapshot. + */ +internal class MarkdownRtlSnapshotTest : PaparazziComposeTest { + + @get:Rule + override val paparazzi: Paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_2.copy(screenHeight = 1200), + renderingMode = SessionParams.RenderingMode.SHRINK, + ) + + @Test + fun `right to left content`() = snapshotWithDarkMode { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + MarkdownText( + """ + > هذا اقتباس طويل بما يكفي لكي يلتف على سطر ثانٍ ونرى أين ينتهي الخط + > + > وهذه فقرة ثانية من الاقتباس نفسه + + - عنصر قائمة طويل بما يكفي لكي يلتف على سطر ثانٍ ونرى مكانه + - عنصر متداخل طويل أيضاً لكي يلتف على سطر ثانٍ + """.trimIndent(), + ) + } + } + + @Composable + private fun MarkdownText(text: String) { + val formatter = MessageTextFormatter.markdownFormatter( + autoTranslationEnabled = false, + typography = ChatTheme.typography, + colors = ChatTheme.colors, + ) + val message = Message(id = "id", cid = "messaging:cid", text = text, user = User(id = "other")) + val styled = formatter.format(message, currentUser = User(id = "me")) + val layout = remember(styled) { mutableStateOf(null) } + Text( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + .blockQuoteRails( + annotations = styled.getStringAnnotations(0, styled.length), + layout = layout::value, + color = ChatTheme.colors.textLowEmphasis, + indentPerDepth = MarkdownStyles.BlockQuoteIndent, + ), + text = styled, + style = ChatTheme.typography.body, + onTextLayout = { layout.value = it }, + ) + } +} diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownSnapshotTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownSnapshotTest.kt new file mode 100644 index 00000000000..adb656189b3 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/MarkdownSnapshotTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.unit.dp +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import io.getstream.chat.android.compose.ui.PaparazziComposeTest +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.User +import org.junit.Rule +import org.junit.Test + +/** + * Renders every supported markdown construct through the markdown formatter, in light and + * dark mode, so that changes to the styling or to the block layout are visible in review. + * + * Span-level styling is asserted precisely in [MarkdownRendererTest]; this covers what only a + * render shows - indentation, line spacing, and how far backgrounds reach. + */ +internal class MarkdownSnapshotTest : PaparazziComposeTest { + + @get:Rule + override val paparazzi: Paparazzi = Paparazzi( + // SHRINK trims to the content, so the device only has to be tall enough not to clip. + deviceConfig = DeviceConfig.PIXEL_2.copy(screenHeight = TallEnoughForEveryConstruct), + renderingMode = SessionParams.RenderingMode.SHRINK, + ) + + @Test + fun `wrapping content`() = snapshotWithDarkMode { + MarkdownText( + """ + - a list item long enough that it wraps onto a second line and shows where that lands + - short + - a nested item long enough to wrap, to see where its continuation lands + - a deeper item, again long enough that it has to wrap onto a second line + - back at the top level, and long enough to wrap so its continuation shows too + + > a quoted line long enough that it wraps, to see whether the marker survives the wrap + """.trimIndent(), + ) + } + + @Test + fun `quote inside a list item`() = snapshotWithDarkMode { + MarkdownText( + """ + - > a quote as the whole item, long enough that it wraps onto a second line + - an ordinary item + + > a quote after a paragraph, also long enough to wrap onto two lines + """.trimIndent(), + ) + } + + @Test + fun `every supported construct`() = snapshotWithDarkMode { + MarkdownText( + """ + Plain, **bold**, *italic*, ***both***, ~~struck~~ and `code()`. + A [link](https://getstream.io) and a bare https://getstream.io too. + A [reference][d] link, and ![an image](https://x.com/a.png) as alt text. + + [d]: https://getstream.io + + # Heading 1 + ## Heading 2 + ### Heading 3 + #### Heading 4 + ##### Heading 5 + ###### Heading 6 + + - first + - second + - nested + - deeper + 1. one + 1. two + + > a quoted line + > and its continuation + > + > a second paragraph, still quoted + + > a separate quote + + > outer, long enough that it wraps onto a second line of its own + > > inner, also long enough that it has to wrap onto a second line + + ```kotlin + fun main() { + println("hi") + } + ``` + + --- + after the break + """.trimIndent(), + ) + } + + @Composable + private fun MarkdownText(text: String) { + val formatter = MessageTextFormatter.markdownFormatter( + autoTranslationEnabled = false, + typography = ChatTheme.typography, + colors = ChatTheme.colors, + ) + val message = Message(id = "id", cid = "messaging:cid", text = text, user = User(id = "other")) + val styled = formatter.format(message, currentUser = User(id = "me")) + // The rails are drawn from the layout, as MessageText draws them, so they show up here too. + val layout = remember(styled) { mutableStateOf(null) } + Text( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + .blockQuoteRails( + annotations = styled.getStringAnnotations(0, styled.length), + layout = layout::value, + color = ChatTheme.colors.textLowEmphasis, + indentPerDepth = MarkdownStyles.BlockQuoteIndent, + ), + text = styled, + style = ChatTheme.typography.body, + onTextLayout = { layout.value = it }, + ) + } +} + +private const val TallEnoughForEveryConstruct = 4000 diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownRtlSnapshotTest_right_to_left_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownRtlSnapshotTest_right_to_left_content.png new file mode 100644 index 0000000000000000000000000000000000000000..4245e607a56f5b4e83372f56f171681c5aa7f912 GIT binary patch literal 94588 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ⅅ>u)cZbn3 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%0An}rZHWeW@v=hOFtoIxSl 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 literal 0 HcmV?d00001 diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownSnapshotTest_every_supported_construct.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownSnapshotTest_every_supported_construct.png new file mode 100644 index 0000000000000000000000000000000000000000..d6d56bd65566d3ff02f7afb2e40ab9119c0bf383 GIT binary patch literal 47485 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 ar@qLu=AT{j%;J~8&%^twiuv-U0sjFb#bv_) literal 0 HcmV?d00001 diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownSnapshotTest_quote_inside_a_list_item.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_MarkdownSnapshotTest_quote_inside_a_list_item.png new file mode 100644 index 0000000000000000000000000000000000000000..8406a0214cade26dc6a39ab417b261874ed72083 GIT binary patch literal 15850 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 literal 0 HcmV?d00001 From c54a776f13bbc97a91e5ca5de095b68714d4ffde Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:36:22 +0200 Subject: [PATCH 2/3] Keep the sample theme setup within the detekt method length --- .../compose/sample/ui/MessagesActivity.kt | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt index 0488f340c1b..29de3336e3c 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt @@ -143,6 +143,29 @@ class MessagesActivity : ComponentActivity() { } } + @Composable + private fun messageTextFormatter( + isInDarkMode: Boolean, + typography: StreamTypography, + shapes: StreamShapes, + colors: StreamColors, + ): MessageTextFormatter = when { + customSettings().isMarkdownEnabled -> MessageTextFormatter.markdownFormatter( + autoTranslationEnabled = ChatApp.autoTranslationEnabled, + isInDarkMode = isInDarkMode, + typography = typography, + colors = colors, + ) + + else -> MessageTextFormatter.defaultFormatter( + autoTranslationEnabled = ChatApp.autoTranslationEnabled, + isInDarkMode = isInDarkMode, + typography = typography, + shapes = shapes, + colors = colors, + ) + } + @Composable private fun SetupChatTheme() { val isInDarkMode = isSystemInDarkTheme() @@ -159,27 +182,12 @@ class MessagesActivity : ComponentActivity() { val ownMessageTheme = MessageTheme.defaultOwnTheme(isInDarkMode, typography, shapes, colors) val attachmentsPickerTabFactories = AttachmentsPickerTabFactories.defaultFactories() + LocationPickerTabFactory(viewModelFactory = SharedLocationViewModelFactory(cid)) - val messageTextFormatter = when { - customSettings().isMarkdownEnabled -> MessageTextFormatter.markdownFormatter( - autoTranslationEnabled = ChatApp.autoTranslationEnabled, - isInDarkMode = isInDarkMode, - typography = typography, - colors = colors, - ) - else -> MessageTextFormatter.defaultFormatter( - autoTranslationEnabled = ChatApp.autoTranslationEnabled, - isInDarkMode = isInDarkMode, - typography = typography, - shapes = shapes, - colors = colors, - ) - } ChatTheme( isInDarkMode = isInDarkMode, colors = colors, shapes = shapes, typography = typography, - messageTextFormatter = messageTextFormatter, + messageTextFormatter = messageTextFormatter(isInDarkMode, typography, shapes, colors), attachmentsPickerTabFactories = attachmentsPickerTabFactories, componentFactory = CustomChatComponentFactory(), dateFormatter = ChatApp.dateFormatter, From 4e80d936f49ce6845943b4e500cc67f97c969dae Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:36:31 +0200 Subject: [PATCH 3/3] Answer a tap with the annotation under it, not the quote it sits in --- .../ui/components/messages/MessageText.kt | 22 +++-- .../messages/MessageTextClickableTagTest.kt | 80 +++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextClickableTagTest.kt diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt index 84f65f8510c..41442b06025 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt @@ -113,10 +113,7 @@ public fun MessageText( ChatTheme.otherMessageTheme.textStyle } } - if (annotations.fastAny { - it.tag == AnnotationTagUrl || it.tag == AnnotationTagEmail || it.tag == AnnotationTagMention - } - ) { + if (annotations.fastAny(AnnotatedString.Range::isClickableTag)) { ClickableText( modifier = modifier .padding( @@ -132,7 +129,9 @@ public fun MessageText( onLongPress = { onLongItemClick(message) }, onTextLayout = { layout.value = it }, ) { position -> - val annotation = annotations.firstOrNull { position in it.start..it.end } + val annotation = annotations.firstOrNull { + it.isClickableTag() && position in it.start until it.end + } if (annotation?.tag == AnnotationTagMention) { message.mentionedUsers.getUserByNameOrId(annotation.item)?.let { onUserMentionClick.invoke(it) } } else { @@ -170,6 +169,19 @@ public fun MessageText( } } +/** + * Whether a tap on this annotation should be acted on. A block quote's annotation covers every + * character of the quote and carries its depth, so leaving it in would answer a tap inside a quote + * with the depth in place of the link, mention or email underneath. + */ +internal fun AnnotatedString.Range.isClickableTag(): Boolean = when (tag) { + AnnotationTagUrl, + AnnotationTagEmail, + AnnotationTagMention, + -> true + else -> false +} + /** * A spin-off of a Foundation component that allows calling long press handlers. * Contains only one additional parameter. diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextClickableTagTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextClickableTagTest.kt new file mode 100644 index 00000000000..28702b80e40 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/MessageTextClickableTagTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.components.messages + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import io.getstream.chat.android.compose.ui.util.AnnotationTagBlockQuote +import io.getstream.chat.android.compose.ui.util.AnnotationTagEmail +import io.getstream.chat.android.compose.ui.util.AnnotationTagLiteral +import io.getstream.chat.android.compose.ui.util.AnnotationTagMention +import io.getstream.chat.android.compose.ui.util.AnnotationTagUrl +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test + +internal class MessageTextClickableTagTest { + + @Test + fun `treats links, emails and mentions as clickable`() { + listOf(AnnotationTagUrl, AnnotationTagEmail, AnnotationTagMention).forEach { tag -> + range(tag).isClickableTag() shouldBeEqualTo true + } + } + + @Test + fun `treats the markers markdown leaves behind as not clickable`() { + listOf(AnnotationTagBlockQuote, AnnotationTagLiteral).forEach { tag -> + range(tag).isClickableTag() shouldBeEqualTo false + } + } + + @Test + fun `resolves a tap inside a quote to the link, not the quote's depth`() { + // A quote's annotation spans its whole text and holds the depth, and it is recorded before + // the entity pass finds the link, so an unfiltered lookup answers with the depth. + val text = buildAnnotatedString { + append("see https://getstream.io") + addStringAnnotation(AnnotationTagBlockQuote, "1", 0, length) + addStringAnnotation(AnnotationTagUrl, "https://getstream.io", 4, length) + } + val position = text.text.indexOf("https") + + val annotations = text.getStringAnnotations(0, text.length) + val resolved = annotations.firstOrNull { + it.isClickableTag() && position in it.start until it.end + } + + resolved?.tag shouldBeEqualTo AnnotationTagUrl + resolved?.item shouldBeEqualTo "https://getstream.io" + // What the lookup used to return. + annotations.firstOrNull { position in it.start until it.end } + ?.tag shouldBeEqualTo AnnotationTagBlockQuote + } + + @Test + fun `does not answer a tap one past the end of an annotation`() { + val text = buildAnnotatedString { + append("hi https://getstream.io there") + addStringAnnotation(AnnotationTagUrl, "https://getstream.io", 3, 23) + } + + val annotations = text.getStringAnnotations(0, text.length) + annotations.firstOrNull { it.isClickableTag() && 23 in it.start until it.end } shouldBeEqualTo null + } + + private fun range(tag: String) = AnnotatedString.Range(item = "value", start = 0, end = 1, tag = tag) +}