diff --git a/README.md b/README.md index 61706199..c60171a2 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ You can download the latest APK [here](https://github.com/alexjyong/android/rele ## Features Added - - @@ -43,10 +43,17 @@ You can download the latest APK [here](https://github.com/alexjyong/android/rele Server Context Menu + + +
+ Emojis next to channel names
Channel Emojis
+ Jump to replied message
- Jump to Reply + Jump to Reply
+ Recently used emojis
+ Recently Used Emojis +
+ The codebase includes the app itself, as well as an internal library for interacting with the Revolt API. The app is written in Kotlin, and wholly uses [Jetpack Compose](https://developer.android.com/jetpack/compose). diff --git a/app/src/main/java/chat/revolt/api/settings/Experiments.kt b/app/src/main/java/chat/revolt/api/settings/Experiments.kt index cfed6040..868d36a9 100644 --- a/app/src/main/java/chat/revolt/api/settings/Experiments.kt +++ b/app/src/main/java/chat/revolt/api/settings/Experiments.kt @@ -26,7 +26,8 @@ class ExperimentInstance(default: Boolean) { * - All experiments can be disabled at once with a single toggle. */ object Experiments { - val useKotlinBasedMarkdownRenderer = ExperimentInstance(true) + val useKotlinBasedMarkdownRenderer = ExperimentInstance(false) + val useEnhancedMarkdownRenderer = ExperimentInstance(true) val usePolar = ExperimentInstance(false) val enableServerIdentityOptions = ExperimentInstance(false) val useFinalMarkdownRenderer = ExperimentInstance(false) @@ -43,6 +44,9 @@ object Experiments { useKotlinBasedMarkdownRenderer.setEnabled( kvStorage.getBoolean("exp/useKotlinBasedMarkdownRenderer") == true ) + useEnhancedMarkdownRenderer.setEnabled( + kvStorage.getBoolean("exp/useEnhancedMarkdownRenderer") == true + ) usePolar.setEnabled( kvStorage.getBoolean("exp/usePolar") == true ) @@ -53,8 +57,16 @@ object Experiments { kvStorage.getBoolean("exp/useFinalMarkdownRenderer") == true ) - if (useFinalMarkdownRenderer.isEnabled && useKotlinBasedMarkdownRenderer.isEnabled) { - // if jbm and fm are enabled, fm takes precedence. this should not be possible in practice + if (useFinalMarkdownRenderer.isEnabled) { + if (useEnhancedMarkdownRenderer.isEnabled) { + useEnhancedMarkdownRenderer.setEnabled(false) + kvStorage.set("exp/useEnhancedMarkdownRenderer", false) + } + if (useKotlinBasedMarkdownRenderer.isEnabled) { + useKotlinBasedMarkdownRenderer.setEnabled(false) + kvStorage.set("exp/useKotlinBasedMarkdownRenderer", false) + } + } else if (useEnhancedMarkdownRenderer.isEnabled && useKotlinBasedMarkdownRenderer.isEnabled) { useKotlinBasedMarkdownRenderer.setEnabled(false) kvStorage.set("exp/useKotlinBasedMarkdownRenderer", false) } diff --git a/app/src/main/java/chat/revolt/composables/generic/EmojiAwareText.kt b/app/src/main/java/chat/revolt/composables/generic/EmojiAwareText.kt new file mode 100644 index 00000000..54166958 --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/generic/EmojiAwareText.kt @@ -0,0 +1,144 @@ +package chat.revolt.composables.generic + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.isUnspecified +import chat.revolt.api.REVOLT_FILES + +/** + * Text composable that automatically detects and renders custom emojis in the format :ULID: + * This bypasses all parser-level emoji handling and works at the Compose rendering level. + */ +@Composable +fun EmojiAwareText( + text: AnnotatedString, + modifier: Modifier = Modifier, + maxLines: Int = Int.MAX_VALUE, + inlineContent: Map = emptyMap(), + onTextLayout: ((androidx.compose.ui.text.TextLayoutResult) -> Unit)? = null +) { + val fontSize = LocalTextStyle.current.fontSize + val emojiSize = if (fontSize.isUnspecified) 16.sp else fontSize + + val customEmojiRegex = Regex(":([0-9A-HJKMNP-TV-Z]{26}):") + val matches = customEmojiRegex.findAll(text.text).toList() + + if (matches.isEmpty()) { + Text( + text = text, + modifier = modifier, + maxLines = maxLines, + inlineContent = inlineContent, + onTextLayout = onTextLayout ?: {} + ) + return + } + + val processedText = buildAnnotatedString { + val sourceText = text.text + var lastIndex = 0 + + matches.forEach { match -> + val emojiId = match.groupValues[1] + val placeholderKey = "emoji_$emojiId" + + if (match.range.first > lastIndex) { + val beforeEmoji = sourceText.substring(lastIndex, match.range.first) + append(beforeEmoji) + + text.spanStyles.forEach { spanStyle -> + if (spanStyle.start < match.range.first && spanStyle.end > lastIndex) { + val spanStart = maxOf(spanStyle.start - lastIndex, 0) + val spanEnd = minOf(spanStyle.end - lastIndex, beforeEmoji.length) + if (spanStart < spanEnd) { + addStyle(spanStyle.item, length - beforeEmoji.length + spanStart, length - beforeEmoji.length + spanEnd) + } + } + } + } + + appendInlineContent(placeholderKey, "[emoji]") + + lastIndex = match.range.last + 1 + } + + if (lastIndex < sourceText.length) { + val remainingText = sourceText.substring(lastIndex) + append(remainingText) + + text.spanStyles.forEach { spanStyle -> + if (spanStyle.start < sourceText.length && spanStyle.end > lastIndex) { + val spanStart = maxOf(spanStyle.start - lastIndex, 0) + val spanEnd = minOf(spanStyle.end - lastIndex, remainingText.length) + if (spanStart < spanEnd) { + addStyle(spanStyle.item, length - remainingText.length + spanStart, length - remainingText.length + spanEnd) + } + } + } + } + } + + val emojiInlineContent = matches.associate { match -> + val emojiId = match.groupValues[1] + val placeholderKey = "emoji_$emojiId" + + placeholderKey to InlineTextContent( + placeholder = Placeholder( + width = emojiSize, + height = emojiSize, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center + ) + ) { + RemoteImage( + url = "$REVOLT_FILES/emojis/$emojiId", + description = ":$emojiId:", + modifier = Modifier + .size(emojiSize.value.dp) + .clip(CircleShape) + ) + } + } + + val mergedInlineContent = inlineContent + emojiInlineContent + + Text( + text = processedText, + inlineContent = mergedInlineContent, + modifier = modifier, + maxLines = maxLines, + onTextLayout = onTextLayout ?: {} + ) +} + +/** + * Overload for plain string input + */ +@Composable +fun EmojiAwareText( + text: String, + modifier: Modifier = Modifier, + maxLines: Int = Int.MAX_VALUE, + inlineContent: Map = emptyMap(), + onTextLayout: ((androidx.compose.ui.text.TextLayoutResult) -> Unit)? = null +) { + EmojiAwareText( + text = AnnotatedString(text), + modifier = modifier, + maxLines = maxLines, + inlineContent = inlineContent, + onTextLayout = onTextLayout + ) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/markdown/MarkdownText.kt b/app/src/main/java/chat/revolt/composables/markdown/MarkdownText.kt index 6fcac15b..721147c4 100644 --- a/app/src/main/java/chat/revolt/composables/markdown/MarkdownText.kt +++ b/app/src/main/java/chat/revolt/composables/markdown/MarkdownText.kt @@ -43,6 +43,7 @@ import chat.revolt.api.routes.custom.fetchEmoji import chat.revolt.api.schemas.isInviteUri import chat.revolt.callbacks.Action import chat.revolt.callbacks.ActionChannel +import chat.revolt.composables.generic.EmojiAwareText import chat.revolt.composables.generic.RemoteImage import chat.revolt.composables.utils.detectTapGesturesConditionalConsume import chat.revolt.internals.EmojiRepository @@ -484,43 +485,9 @@ fun MarkdownText(textNode: AstNode, modifier: Modifier = Modifier) { false } - Text( + EmojiAwareText( text = annotatedText, - style = LocalTextStyle.current, - color = LocalContentColor.current, onTextLayout = { layoutResult = it }, - inlineContent = mapOf( - "CustomEmote" to InlineTextContent( - placeholder = Placeholder( - width = LocalTextStyle.current.fontSize * 1.5, - height = LocalTextStyle.current.fontSize * 1.5, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center - ), - ) { id -> - val emote = RevoltAPI.emojiCache[id] - if (emote == null) { - scope.launch { - try { - RevoltAPI.emojiCache[id] = fetchEmoji(id) - } catch (e: Exception) { - // no-op - } - } - return@InlineTextContent - } else { - with(LocalDensity.current) { - RemoteImage( - url = "$REVOLT_FILES/emojis/${id}", - description = emote.name, - contentScale = ContentScale.Fit, - modifier = Modifier - .width((LocalTextStyle.current.fontSize * 1.5).toDp()) - .height((LocalTextStyle.current.fontSize * 1.5).toDp()) - ) - } - } - } - ), modifier = modifier.pointerInput(onClick, onLongClick) { detectTapGesturesConditionalConsume( onTap = { pos -> diff --git a/app/src/main/java/chat/revolt/composables/markdown/RichMarkdown.kt b/app/src/main/java/chat/revolt/composables/markdown/RichMarkdown.kt index 492dffb2..31883a02 100644 --- a/app/src/main/java/chat/revolt/composables/markdown/RichMarkdown.kt +++ b/app/src/main/java/chat/revolt/composables/markdown/RichMarkdown.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier import chat.revolt.api.settings.Experiments import chat.revolt.markdown.jbm.JBM import chat.revolt.markdown.jbm.JBMRenderer +import chat.revolt.markdown.jbm.JBMEnhancedRenderer import chat.revolt.ndk.Stendal @OptIn(JBM::class) @@ -16,10 +17,15 @@ fun RichMarkdown(input: String, modifier: Modifier = Modifier) { Experiments.useFinalMarkdownRenderer.isEnabled -> { JBMRenderer(input) } - else -> { + Experiments.useEnhancedMarkdownRenderer.isEnabled -> { + JBMEnhancedRenderer(input) + } + Experiments.useKotlinBasedMarkdownRenderer.isEnabled -> { JBMRenderer(input) } + else -> { + JBMEnhancedRenderer(input) + } } - } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/JBMApi.kt b/app/src/main/java/chat/revolt/markdown/jbm/JBMApi.kt index 4ce186b1..fdd13a7c 100644 --- a/app/src/main/java/chat/revolt/markdown/jbm/JBMApi.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/JBMApi.kt @@ -10,7 +10,7 @@ annotation class JBM @JBM object JBMApi { - fun parse(src: String): ASTNode { - return MarkdownParser(RSMFlavourDescriptor()).buildMarkdownTreeFromString(src) + fun parse(src: String, flavor: RSMFlavourDescriptor = RSMFlavourDescriptor()): ASTNode { + return MarkdownParser(flavor).buildMarkdownTreeFromString(src) } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/JBMEnhancedRenderer.kt b/app/src/main/java/chat/revolt/markdown/jbm/JBMEnhancedRenderer.kt new file mode 100644 index 00000000..47c526ab --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/JBMEnhancedRenderer.kt @@ -0,0 +1,23 @@ +package chat.revolt.markdown.jbm + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier + +/** + * Enhanced JBM Renderer that provides additional markdown features. + * This renderer inherits all functionality from the original JBMRenderer while adding support + * for spoilers and other enhancements through a simple preprocessing approach. + */ + +@JBM +@Composable +fun JBMEnhancedRenderer(content: String, modifier: Modifier = Modifier) { + CompositionLocalProvider( + LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy( + enhanced = true + ) + ) { + JBMRenderer(content, modifier) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/JBMRenderer.kt b/app/src/main/java/chat/revolt/markdown/jbm/JBMRenderer.kt index 2b9d7c05..0450008c 100644 --- a/app/src/main/java/chat/revolt/markdown/jbm/JBMRenderer.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/JBMRenderer.kt @@ -83,6 +83,7 @@ import chat.revolt.api.schemas.isInviteUri import chat.revolt.api.settings.LoadedSettings import chat.revolt.callbacks.Action import chat.revolt.callbacks.ActionChannel +import chat.revolt.composables.generic.EmojiAwareText import chat.revolt.composables.generic.RemoteImage import chat.revolt.composables.generic.UserAvatar import chat.revolt.composables.markdown.Annotations @@ -138,6 +139,7 @@ data class JBMarkdownTreeState( val currentServer: String? = null, val embedded: Boolean = false, val singleLine: Boolean = false, + val enhanced: Boolean = false, val colors: JBMColors = JBMColors( clickable = Color(0xFFFF00FF), clickableBackground = Color(0x2000FF00) @@ -153,11 +155,14 @@ val avatarPadding = 2.dp @Composable @JBM fun JBMRenderer(content: String, modifier: Modifier = Modifier) { - var tree by remember { mutableStateOf(JBMApi.parse(content)) } + val state = LocalJBMarkdownTreeState.current + val flavor = if (state.enhanced) RSMEnhancedFlavourDescriptor() else RSMFlavourDescriptor() + + var tree by remember { mutableStateOf(JBMApi.parse(content, flavor)) } var revealedSpoilers by remember { mutableStateOf(setOf()) } - LaunchedEffect(content) { - tree = JBMApi.parse(content) + LaunchedEffect(content, state.enhanced) { + tree = JBMApi.parse(content, flavor) } CompositionLocalProvider( @@ -324,23 +329,6 @@ private fun annotateText( } } - RSMElementTypes.CUSTOM_EMOTE -> { - val contents = node.getTextInNode(sourceText).toString() - val emoteId = contents.removeSurrounding(":", ":") - if (emoteId == contents || !emoteId.isUlid()) { - // Invalid custom emote. Append as if it were regular text. - for (child in node.children) { - append(annotateText(state, child, revealedSpoilers)) - } - } else { - pushStringAnnotation( - tag = JBMAnnotations.CustomEmote.tag, - annotation = emoteId - ) - appendInlineContent(JBMAnnotations.CustomEmote.tag, emoteId) - pop() - } - } RSMElementTypes.SPOILER -> { android.util.Log.d("JBMRenderer", "Found SPOILER node with ${node.children.size} children") @@ -714,14 +702,6 @@ private fun JBMText(node: ASTNode, modifier: Modifier) { return@handler true } - JBMAnnotations.CustomEmote.tag -> { - scope.launch { - ActionChannel.send( - Action.EmoteInfo(item) - ) - } - return@handler true - } JBMAnnotations.Spoiler.tag -> { mdState.onSpoilerToggle?.invoke(item) @@ -753,11 +733,10 @@ private fun JBMText(node: ASTNode, modifier: Modifier) { return@handler false } - Text( + EmojiAwareText( text = annotatedText, onTextLayout = { layoutResult = it }, maxLines = if (mdState.singleLine) 1 else Int.MAX_VALUE, - overflow = if (mdState.singleLine) TextOverflow.Ellipsis else TextOverflow.Clip, modifier = modifier.pointerInput(onClick, onLongClick) { detectTapGesturesConditionalConsume( onTap = { pos -> @@ -851,36 +830,6 @@ private fun JBMText(node: ASTNode, modifier: Modifier) { } } }, - JBMAnnotations.CustomEmote.tag to InlineTextContent( - placeholder = Placeholder( - width = LocalTextStyle.current.fontSize * 1.5, - height = LocalTextStyle.current.fontSize * 1.5, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center - ), - ) { id -> - val emote = RevoltAPI.emojiCache[id] - if (emote == null) { - scope.launch { - try { - RevoltAPI.emojiCache[id] = fetchEmoji(id) - } catch (e: Exception) { - // no-op - } - } - return@InlineTextContent - } else { - with(LocalDensity.current) { - RemoteImage( - url = "$REVOLT_FILES/emojis/${id}", - description = emote.name, - contentScale = ContentScale.Fit, - modifier = Modifier - .width((LocalTextStyle.current.fontSize * 1.5).toDp()) - .height((LocalTextStyle.current.fontSize * 1.5).toDp()) - ) - } - } - }, JBMAnnotations.UserAvatar.tag to with(LocalDensity.current) { val placeholderBaseWidth = (LocalTextStyle.current.fontSize * 1.5).toPx() - (avatarPadding * 2).toPx() diff --git a/app/src/main/java/chat/revolt/markdown/jbm/RSMEnhancedFlavourDescriptor.kt b/app/src/main/java/chat/revolt/markdown/jbm/RSMEnhancedFlavourDescriptor.kt new file mode 100644 index 00000000..e336d756 --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/RSMEnhancedFlavourDescriptor.kt @@ -0,0 +1,20 @@ +package chat.revolt.markdown.jbm + +import chat.revolt.markdown.jbm.sequentialparsers.SpoilerParser +import org.intellij.markdown.parser.sequentialparsers.SequentialParser +import org.intellij.markdown.parser.sequentialparsers.SequentialParserManager + +/** + * Enhanced RSM Flavour Descriptor that inherits from the original working RSMFlavourDescriptor. + * This enhanced version adds support for spoilers by carefully adding SpoilerParser + * at the end of the parsing sequence to avoid conflicts with existing parsers. + */ +class RSMEnhancedFlavourDescriptor : RSMFlavourDescriptor() { + override val sequentialParserManager = object : SequentialParserManager() { + override fun getParserSequence(): List { + val originalParsers = super@RSMEnhancedFlavourDescriptor.sequentialParserManager.getParserSequence() + + return originalParsers + SpoilerParser() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt b/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt index cffda89d..798f5e30 100644 --- a/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt @@ -1,10 +1,8 @@ package chat.revolt.markdown.jbm import chat.revolt.markdown.jbm.sequentialparsers.ChannelMentionParser -import chat.revolt.markdown.jbm.sequentialparsers.CustomEmoteParser import chat.revolt.markdown.jbm.sequentialparsers.MassMentionParser import chat.revolt.markdown.jbm.sequentialparsers.RoleMentionParser -import chat.revolt.markdown.jbm.sequentialparsers.SpoilerParser import chat.revolt.markdown.jbm.sequentialparsers.UserMentionParser import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor @@ -21,7 +19,7 @@ import org.intellij.markdown.parser.sequentialparsers.impl.InlineLinkParser import org.intellij.markdown.parser.sequentialparsers.impl.MathParser import org.intellij.markdown.parser.sequentialparsers.impl.ReferenceLinkParser -class RSMFlavourDescriptor : GFMFlavourDescriptor() { +open class RSMFlavourDescriptor : GFMFlavourDescriptor() { override val sequentialParserManager = object : SequentialParserManager() { override fun getParserSequence(): List { return listOf( @@ -35,9 +33,7 @@ class RSMFlavourDescriptor : GFMFlavourDescriptor() { UserMentionParser(), ChannelMentionParser(), RoleMentionParser(), - MassMentionParser(), - CustomEmoteParser(), - SpoilerParser() + MassMentionParser() ) } } diff --git a/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/CustomEmoteParser.kt b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/CustomEmoteParser.kt index 95ac1eb8..b3acf8d5 100644 --- a/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/CustomEmoteParser.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/CustomEmoteParser.kt @@ -16,11 +16,14 @@ class CustomEmoteParser : SequentialParser { var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue) while (iterator.type != null) { + android.util.Log.d("CustomEmoteParser", "Processing token: type=${iterator.type}, text='${iterator.toString()}'") + if (iterator.type == MarkdownTokenTypes.COLON) { - + android.util.Log.d("CustomEmoteParser", "Found COLON token") val endIterator = findNextColon(iterator.advance()) if (endIterator != null) { + android.util.Log.d("CustomEmoteParser", "Found matching closing colon, creating CUSTOM_EMOTE node") result.withNode( SequentialParser.Node( iterator.index..endIterator.index + 1, diff --git a/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt index e0c33cd7..c45b0c75 100644 --- a/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt @@ -47,7 +47,7 @@ import chat.revolt.settings.dsl.SubcategoryContentInsets import kotlinx.coroutines.launch enum class MarkdownRenderer { - JetBrains, FinalMarkdown + JetBrains, JetBrainsEnhanced, FinalMarkdown } class ExperimentsSettingsScreenViewModel : ViewModel() { @@ -56,17 +56,21 @@ class ExperimentsSettingsScreenViewModel : ViewModel() { fun init() { viewModelScope.launch { when { - Experiments.useKotlinBasedMarkdownRenderer.isEnabled -> { - mdRenderer.value = MarkdownRenderer.JetBrains - } - Experiments.useFinalMarkdownRenderer.isEnabled -> { mdRenderer.value = MarkdownRenderer.FinalMarkdown } - else -> { + Experiments.useEnhancedMarkdownRenderer.isEnabled -> { + mdRenderer.value = MarkdownRenderer.JetBrainsEnhanced + } + + Experiments.useKotlinBasedMarkdownRenderer.isEnabled -> { mdRenderer.value = MarkdownRenderer.JetBrains } + + else -> { + mdRenderer.value = MarkdownRenderer.JetBrainsEnhanced + } } usePolarChecked.value = Experiments.usePolar.isEnabled enableServerIdentityOptionsChecked.value = @@ -98,7 +102,7 @@ class ExperimentsSettingsScreenViewModel : ViewModel() { } } - val mdRenderer = mutableStateOf(MarkdownRenderer.JetBrains) + val mdRenderer = mutableStateOf(MarkdownRenderer.JetBrainsEnhanced) fun setMdRenderer(value: MarkdownRenderer) { viewModelScope.launch { @@ -106,6 +110,17 @@ class ExperimentsSettingsScreenViewModel : ViewModel() { MarkdownRenderer.JetBrains -> { kv.set("exp/useKotlinBasedMarkdownRenderer", true) Experiments.useKotlinBasedMarkdownRenderer.setEnabled(true) + kv.set("exp/useEnhancedMarkdownRenderer", false) + Experiments.useEnhancedMarkdownRenderer.setEnabled(false) + kv.set("exp/useFinalMarkdownRenderer", false) + Experiments.useFinalMarkdownRenderer.setEnabled(false) + } + + MarkdownRenderer.JetBrainsEnhanced -> { + kv.set("exp/useKotlinBasedMarkdownRenderer", false) + Experiments.useKotlinBasedMarkdownRenderer.setEnabled(false) + kv.set("exp/useEnhancedMarkdownRenderer", true) + Experiments.useEnhancedMarkdownRenderer.setEnabled(true) kv.set("exp/useFinalMarkdownRenderer", false) Experiments.useFinalMarkdownRenderer.setEnabled(false) } @@ -113,6 +128,8 @@ class ExperimentsSettingsScreenViewModel : ViewModel() { MarkdownRenderer.FinalMarkdown -> { kv.set("exp/useKotlinBasedMarkdownRenderer", false) Experiments.useKotlinBasedMarkdownRenderer.setEnabled(false) + kv.set("exp/useEnhancedMarkdownRenderer", false) + Experiments.useEnhancedMarkdownRenderer.setEnabled(false) kv.set("exp/useFinalMarkdownRenderer", true) Experiments.useFinalMarkdownRenderer.setEnabled(true) } @@ -203,7 +220,8 @@ fun ExperimentsSettingsScreen( }, supportingContent = { when (viewModel.mdRenderer.value) { - MarkdownRenderer.JetBrains -> Text("Use the Kotlin-based JetBrains Markdown renderer for messages. This renderer is feature-complete with support for strikethrough, spoilers, and more. (Default)") + MarkdownRenderer.JetBrains -> Text("Use the original Kotlin-based JetBrains Markdown renderer for messages. Stable but without spoiler support.") + MarkdownRenderer.JetBrainsEnhanced -> Text("Use the enhanced JetBrains Markdown renderer with spoiler support and other improvements. Recommended for best experience. (Default)") MarkdownRenderer.FinalMarkdown -> Text("Use a new blazingly fast markdown renderer for messages. This renderer is experimental and may have missing features.") } }, @@ -226,6 +244,15 @@ fun ExperimentsSettingsScreen( ) { Text("JetBrains") } + ToggleButton( + checked = viewModel.mdRenderer.value == MarkdownRenderer.JetBrainsEnhanced, + onCheckedChange = { viewModel.setMdRenderer(MarkdownRenderer.JetBrainsEnhanced) }, + modifier = Modifier + .fillMaxWidth() + .semantics { role = Role.RadioButton } + ) { + Text("JetBrains Enhanced") + } if (FeatureFlags.finalMarkdownGranted || viewModel.mdRenderer.value == MarkdownRenderer.FinalMarkdown) { ToggleButton( checked = viewModel.mdRenderer.value == MarkdownRenderer.FinalMarkdown,