Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -141,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,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

else -> MessageTextFormatter.defaultFormatter(
autoTranslationEnabled = ChatApp.autoTranslationEnabled,
isInDarkMode = isInDarkMode,
typography = typography,
shapes = shapes,
colors = colors,
)
}

@Composable
private fun SetupChatTheme() {
val isInDarkMode = isSystemInDarkTheme()
Expand All @@ -162,6 +187,7 @@ class MessagesActivity : ComponentActivity() {
colors = colors,
shapes = shapes,
typography = typography,
messageTextFormatter = messageTextFormatter(isInDarkMode, typography, shapes, colors),
attachmentsPickerTabFactories = attachmentsPickerTabFactories,
componentFactory = CustomChatComponentFactory(),
dateFormatter = ChatApp.dateFormatter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() &&
Expand All @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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,
) {
Expand All @@ -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,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
<string name="custom_login_hint_user_name">Username (optional)</string>
<string name="custom_login_enable_adaptive_layout">Enable adaptive layout (Experimental)</string>
<string name="custom_login_enable_adaptive_layout_description">Adjust layout based on screen sizes</string>
<string name="custom_login_enable_markdown">Enable markdown</string>
<string name="custom_login_enable_markdown_description">Render message text as markdown</string>

<!-- Pinned Messages -->
<string name="pinned_messages_title">Pinned Messages</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions stream-chat-android-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,6 +43,8 @@
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
Expand All @@ -67,7 +70,7 @@
*/
@Composable
@Suppress("LongMethod")
public fun MessageText(

Check failure on line 73 in stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageText.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 27 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-android&issues=AaCAc0_eUcM4pixfx4bi&open=AaCAc0_eUcM4pixfx4bi&pullRequest=6683
message: Message,
currentUser: User?,
modifier: Modifier = Modifier,
Expand All @@ -89,7 +92,16 @@
}
}

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<TextLayoutResult?>(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 {
Expand All @@ -101,10 +113,7 @@
ChatTheme.otherMessageTheme.textStyle
}
}
if (annotations.fastAny {
it.tag == AnnotationTagUrl || it.tag == AnnotationTagEmail || it.tag == AnnotationTagMention
}
) {
if (annotations.fastAny(AnnotatedString.Range<String>::isClickableTag)) {
ClickableText(
modifier = modifier
.padding(
Expand All @@ -113,21 +122,30 @@
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 }
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 {
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.
}
}
}
}
Expand All @@ -142,13 +160,28 @@
vertical = verticalPadding,
)
.clipToBounds()
.testTag("Stream_MessageText"),
.testTag("Stream_MessageText")
.then(quoteRails),
text = styledText,
style = style,
onTextLayout = { layout.value = it },
)
}
}

/**
* 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<String>.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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Loading
Loading