feat(jbm): continue development

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-09-13 00:04:00 +02:00
parent 5497933589
commit 7cfbc3fdac
5 changed files with 333 additions and 51 deletions

View File

@ -269,6 +269,7 @@ dependencies {
// Markup // Markup
implementation "org.jetbrains:markdown:0.7.3" implementation "org.jetbrains:markdown:0.7.3"
implementation "dev.snipme:highlights:0.9.1"
// Livekit // Livekit
// FIXME temporarily not included, re-add when realtime media is to be implemented // FIXME temporarily not included, re-add when realtime media is to be implemented

View File

@ -34,13 +34,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -78,6 +82,9 @@ import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.UserAvatarWidthPlaceholder import chat.revolt.components.generic.UserAvatarWidthPlaceholder
import chat.revolt.components.markdown.LocalMarkdownTreeConfig import chat.revolt.components.markdown.LocalMarkdownTreeConfig
import chat.revolt.components.markdown.RichMarkdown import chat.revolt.components.markdown.RichMarkdown
import chat.revolt.components.markdown.jbm.JBM
import chat.revolt.components.markdown.jbm.JBMRenderer
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
import chat.revolt.internals.text.Gigamoji import chat.revolt.internals.text.Gigamoji
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import chat.revolt.api.schemas.Message as MessageSchema import chat.revolt.api.schemas.Message as MessageSchema
@ -167,7 +174,7 @@ fun formatLongAsTime(time: Long): String {
} }
} }
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class, JBM::class)
@Composable @Composable
fun Message( fun Message(
message: MessageSchema, message: MessageSchema,
@ -192,6 +199,8 @@ fun Message(
val authorIsBlocked = remember(author) { author.relationship == "Blocked" } val authorIsBlocked = remember(author) { author.relationship == "Blocked" }
var __TEMPORARY_useJbm by remember { mutableStateOf(false) }
Column(Modifier.animateContentSize()) { Column(Modifier.animateContentSize()) {
if (message.tail == false) { if (message.tail == false) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
@ -361,17 +370,40 @@ fun Message(
message.content?.let { message.content?.let {
if (message.content.isBlank()) return@let // if only an attachment is sent if (message.content.isBlank()) return@let // if only an attachment is sent
CompositionLocalProvider( Switch(
LocalMarkdownTreeConfig provides LocalMarkdownTreeConfig.current.copy( checked = __TEMPORARY_useJbm,
currentServer = RevoltAPI.channelCache[message.channel]?.server, onCheckedChange = { __TEMPORARY_useJbm = it },
fontSizeMultiplier = Gigamoji.useGigamojiForMessage(message.content) )
.let {
if (it) 2f else 1f if (__TEMPORARY_useJbm == false) {
} CompositionLocalProvider(
) LocalMarkdownTreeConfig provides LocalMarkdownTreeConfig.current.copy(
) { currentServer = RevoltAPI.channelCache[message.channel]?.server,
Spacer(modifier = Modifier.height(2.dp)) fontSizeMultiplier = Gigamoji.useGigamojiForMessage(
RichMarkdown(input = message.content) message.content
)
.let {
if (it) 2f else 1f
}
)
) {
Spacer(modifier = Modifier.height(2.dp))
RichMarkdown(input = message.content)
}
} else {
CompositionLocalProvider(
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
fontSizeMultiplier = Gigamoji.useGigamojiForMessage(
message.content
)
.let {
if (it) 2f else 1f
}
)
) {
Spacer(modifier = Modifier.height(2.dp))
JBMRenderer(message.content)
}
} }
} }
} }

View File

@ -1,10 +1,18 @@
package chat.revolt.components.markdown.jbm package chat.revolt.components.markdown.jbm
import android.content.res.Configuration
import android.util.Log import android.util.Log
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@ -20,12 +28,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.Placeholder
@ -39,8 +49,17 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.revolt.api.settings.GlobalState
import chat.revolt.components.markdown.Annotations import chat.revolt.components.markdown.Annotations
import chat.revolt.components.utils.detectTapGesturesConditionalConsume import chat.revolt.components.utils.detectTapGesturesConditionalConsume
import chat.revolt.ui.theme.FragmentMono
import chat.revolt.ui.theme.isThemeDark
import dev.snipme.highlights.Highlights
import dev.snipme.highlights.model.BoldHighlight
import dev.snipme.highlights.model.CodeHighlight
import dev.snipme.highlights.model.ColorHighlight
import dev.snipme.highlights.model.SyntaxLanguage
import dev.snipme.highlights.model.SyntaxThemes
import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.ast.ASTNode
@ -50,10 +69,11 @@ import org.intellij.markdown.flavours.gfm.GFMTokenTypes
data class JBMarkdownTreeState( data class JBMarkdownTreeState(
val sourceText: String = "", val sourceText: String = "",
val ignoreLineBreaks: Boolean = false,
val listDepth: Int = 0, val listDepth: Int = 0,
val fontSizeMultiplier: Float = 1f, val fontSizeMultiplier: Float = 1f,
val linksClickable: Boolean = true val linksClickable: Boolean = true,
val currentServer: String? = null,
val embedded: Boolean = false,
) )
val LocalJBMarkdownTreeState = val LocalJBMarkdownTreeState =
@ -66,15 +86,21 @@ fun JBMRenderer(content: String, modifier: Modifier = Modifier) {
LaunchedEffect(content) { LaunchedEffect(content) {
tree = JBMApi.parse(content) tree = JBMApi.parse(content)
Log.d("JBMRenderer", "Parsed tree: ${tree.children.map { it.type.name }}")
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalJBMarkdownTreeState provides JBMarkdownTreeState(content) LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
sourceText = content
)
) { ) {
tree.children.map { if (LocalJBMarkdownTreeState.current.embedded) {
JBMBlock(it, modifier) tree.children.getOrNull(0)?.let {
JBMBlock(it, modifier)
}
} else {
tree.children.map {
JBMBlock(it, modifier)
}
} }
} }
} }
@ -89,12 +115,34 @@ private fun annotateText(
buildAnnotatedString { buildAnnotatedString {
when (node.type) { when (node.type) {
MarkdownTokenTypes.TEXT -> { MarkdownTokenTypes.TEXT -> {
append(node.getTextInNode(sourceText)) val source = if (state.embedded) {
node.getTextInNode(sourceText).toString().replace("\n", " ")
} else {
node.getTextInNode(sourceText)
}
append(source)
}
MarkdownTokenTypes.ATX_HEADER -> {
// Do not need to do anything
}
MarkdownElementTypes.ATX_1,
MarkdownElementTypes.ATX_2,
MarkdownElementTypes.ATX_3,
MarkdownElementTypes.ATX_4,
MarkdownElementTypes.ATX_5,
MarkdownElementTypes.ATX_6 -> {
for (child in node.children) {
append(annotateText(state, child))
}
} }
MarkdownElementTypes.EMPH -> { MarkdownElementTypes.EMPH -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) { withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
for (child in node.children) { // Skip the first child and the last child
// because they are the asterisk characters
for (child in node.children.subList(1, node.children.size - 1)) {
append(annotateText(state, child)) append(annotateText(state, child))
} }
} }
@ -102,7 +150,9 @@ private fun annotateText(
MarkdownElementTypes.STRONG -> { MarkdownElementTypes.STRONG -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
for (child in node.children) { // Skip the first two children and the last two children
// because they are the asterisk characters
for (child in node.children.subList(2, node.children.size - 2)) {
append(annotateText(state, child)) append(annotateText(state, child))
} }
} }
@ -118,8 +168,24 @@ private fun annotateText(
} }
} }
MarkdownElementTypes.CODE_SPAN -> {
withStyle(SpanStyle(fontFamily = FragmentMono)) {
val startsWithTwoBackticks =
node.children.getOrNull(1)?.type == MarkdownTokenTypes.BACKTICK
val removeItemCount = if (startsWithTwoBackticks) 2 else 1
// Skip the first and last 1 or 2 children
// because they are the backtick characters
for (child in node.children.subList(
removeItemCount,
node.children.size - removeItemCount
)) {
append(annotateText(state, child))
}
}
}
MarkdownTokenTypes.LIST_BULLET -> { MarkdownTokenTypes.LIST_BULLET -> {
append(" ".repeat(state.listDepth) + " " + if (state.listDepth % 2 == 0) "" else "" + " ") append(" ".repeat(state.listDepth) + " ")
} }
MarkdownTokenTypes.LIST_NUMBER -> { MarkdownTokenTypes.LIST_NUMBER -> {
@ -145,7 +211,9 @@ private fun annotateText(
append(" ") append(" ")
} }
MarkdownElementTypes.PARAGRAPH, MarkdownElementTypes.HTML_BLOCK -> { MarkdownElementTypes.PARAGRAPH,
MarkdownElementTypes.HTML_BLOCK,
MarkdownTokenTypes.ATX_CONTENT -> {
for (child in node.children) { for (child in node.children) {
append(annotateText(state, child)) append(annotateText(state, child))
} }
@ -167,15 +235,11 @@ private fun annotateText(
MarkdownTokenTypes.EOL, MarkdownTokenTypes.EOL,
MarkdownTokenTypes.WHITE_SPACE, MarkdownTokenTypes.WHITE_SPACE,
MarkdownTokenTypes.COLON, MarkdownTokenTypes.COLON,
MarkdownTokenTypes.EMPH,
GFMTokenTypes.TILDE -> { GFMTokenTypes.TILDE -> {
append(node.getTextInNode(sourceText)) append(node.getTextInNode(sourceText))
} }
// no-op types
// for example, the special characters that are used to denote the markup are here
MarkdownTokenTypes.EMPH -> {
}
else -> { else -> {
append("[${node.type.name}]{\n") append("[${node.type.name}]{\n")
append(node.getTextInNode(sourceText)) append(node.getTextInNode(sourceText))
@ -292,14 +356,132 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
) )
} }
private fun annotateHighlights(
source: String,
highlights: List<CodeHighlight>
): AnnotatedString {
val highlightStyles = highlights.map {
when (it) {
is BoldHighlight -> AnnotatedString.Range(
SpanStyle(fontWeight = FontWeight.Bold),
it.location.start,
it.location.end
)
is ColorHighlight -> {
AnnotatedString.Range(
SpanStyle(color = Color(0xFF000000 or it.rgb.toLong())),
it.location.start,
it.location.end
)
}
else -> null
}
}.filterNotNull()
return AnnotatedString(source, spanStyles = highlightStyles)
}
// ======== TODO ========
// - Add aliases for languages. For example, "js" should be an alias for "javascript" and "ts" should be an alias for "typescript", etc.
// - Better looking language name display. Ideally a dictionary of language names to display names with proper brand casing.
@Composable
private fun JBMCodeBlockContent(node: ASTNode, modifier: Modifier) {
val state = LocalJBMarkdownTreeState.current
/*val colours = MaterialTheme.colorScheme
val contentColour = LocalContentColor.current
val syntaxTheme = remember {
SyntaxTheme(
key = "chat.revolt.M3Dynamic",
code = contentColour.toArgb() and 0xFFFFFF,
comment = colours.outline.toArgb() and 0xFFFFFF,
multilineComment = colours.outline.toArgb() and 0xFFFFFF,
keyword = colours.primary.toArgb() and 0xFFFFFF,
string = colours.secondary.toArgb() and 0xFFFFFF,
literal = colours.tertiary.toArgb() and 0xFFFFFF,
mark = colours.error.toArgb() and 0xFFFFFF,
punctuation = colours.inversePrimary.toArgb() and 0xFFFFFF,
metadata = colours.inverseSurface.toArgb() and 0xFFFFFF,
)
}*/
val uiMode = LocalConfiguration.current.uiMode
val systemIsDark =
(uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val themeIsDark = remember(GlobalState.theme) { isThemeDark(GlobalState.theme, systemIsDark) }
val codeFenceLanguage = remember(node) {
node.children.firstOrNull { it.type == MarkdownTokenTypes.FENCE_LANG }
?.getTextInNode(state.sourceText)?.toString()
}
val codeFenceContent = remember(node) {
node.children
.filter { it.type == MarkdownTokenTypes.CODE_FENCE_CONTENT || it.type == MarkdownTokenTypes.EOL }
.joinToString("") {
it.getTextInNode(state.sourceText).toString()
}
.trim()
}
val annotatedContent = remember(codeFenceLanguage, codeFenceContent) {
val canAnnotate = codeFenceLanguage != null
val language = codeFenceLanguage?.let { SyntaxLanguage.getByName(it) }
val shouldAnnotate = language != null
if (canAnnotate && shouldAnnotate) {
buildAnnotatedString {
val highlights = Highlights.Builder().apply {
code(codeFenceContent)
language(language ?: SyntaxLanguage.DEFAULT)
theme(SyntaxThemes.notepad(themeIsDark))
//theme(syntaxTheme)
}.build()
append(annotateHighlights(codeFenceContent, highlights.getHighlights()))
}
} else {
buildAnnotatedString {
append(codeFenceContent)
}
}
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(8.dp)
) {
if (codeFenceLanguage != null) {
Text(
text = codeFenceLanguage,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 4.dp)
)
}
Box(
modifier = Modifier.horizontalScroll(rememberScrollState())
) {
Text(
text = annotatedContent,
fontFamily = FragmentMono,
modifier = Modifier
)
}
}
}
@Composable @Composable
private fun JBMBlock(node: ASTNode, modifier: Modifier) { private fun JBMBlock(node: ASTNode, modifier: Modifier) {
val state = LocalJBMarkdownTreeState.current
when (node.type) { when (node.type) {
MarkdownElementTypes.PARAGRAPH, MarkdownElementTypes.PARAGRAPH,
MarkdownElementTypes.HTML_BLOCK -> { MarkdownElementTypes.HTML_BLOCK -> {
CompositionLocalProvider( CompositionLocalProvider(
LocalTextStyle provides LocalTextStyle.current.copy( LocalTextStyle provides LocalTextStyle.current.copy(
fontSize = LocalTextStyle.current.fontSize * LocalJBMarkdownTreeState.current.fontSizeMultiplier fontSize = LocalTextStyle.current.fontSize * state.fontSizeMultiplier
) )
) { ) {
JBMText(node, modifier) JBMText(node, modifier)
@ -315,42 +497,44 @@ private fun JBMBlock(node: ASTNode, modifier: Modifier) {
CompositionLocalProvider( CompositionLocalProvider(
LocalTextStyle provides LocalTextStyle.current.copy( LocalTextStyle provides LocalTextStyle.current.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = when (node.type) { fontSize = if (state.embedded) LocalTextStyle.current.fontSize
MarkdownElementTypes.ATX_1 -> 32.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier else when (node.type) {
MarkdownElementTypes.ATX_2 -> 24.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier MarkdownElementTypes.ATX_1 -> 32.sp * state.fontSizeMultiplier
MarkdownElementTypes.ATX_3 -> 20.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier MarkdownElementTypes.ATX_2 -> 24.sp * state.fontSizeMultiplier
MarkdownElementTypes.ATX_4 -> 16.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier MarkdownElementTypes.ATX_3 -> 20.sp * state.fontSizeMultiplier
MarkdownElementTypes.ATX_5 -> 14.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier MarkdownElementTypes.ATX_4 -> 16.sp * state.fontSizeMultiplier
else -> 12.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier MarkdownElementTypes.ATX_5 -> 14.sp * state.fontSizeMultiplier
}, else -> 12.sp * state.fontSizeMultiplier
color = when (node.type) {
MarkdownElementTypes.ATX_1 -> Color(0xFFFF0000)
MarkdownElementTypes.ATX_2 -> Color(0xFF00FF00)
MarkdownElementTypes.ATX_3 -> Color(0xFF0000FF)
MarkdownElementTypes.ATX_4 -> Color(0xFFFF00FF)
MarkdownElementTypes.ATX_5 -> Color(0xFF00FFFF)
else -> Color(0xFFFFFF00)
} }
) )
) { ) {
if (node.startOffset != 0) { if (node.startOffset != 0) {
Box(Modifier.padding(top = 8.dp)) Spacer(Modifier.height(8.dp))
} }
JBMText(node, modifier) JBMText(node, modifier)
Spacer(Modifier.height(4.dp))
} }
} }
MarkdownElementTypes.ORDERED_LIST, MarkdownElementTypes.ORDERED_LIST,
MarkdownElementTypes.UNORDERED_LIST -> { MarkdownElementTypes.UNORDERED_LIST -> {
CompositionLocalProvider( CompositionLocalProvider(
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy( LocalJBMarkdownTreeState provides state.copy(
listDepth = LocalJBMarkdownTreeState.current.listDepth + 1 listDepth = state.listDepth + 1
) )
) { ) {
JBMText(node, modifier) JBMText(node, modifier)
} }
} }
MarkdownTokenTypes.EOL -> {
Spacer(Modifier.height(4.dp))
}
MarkdownElementTypes.CODE_FENCE -> {
JBMCodeBlockContent(node, modifier)
}
else -> { else -> {
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {

View File

@ -1,19 +1,27 @@
package chat.revolt.screens.labs.ui.sandbox package chat.revolt.screens.labs.ui.sandbox
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.components.markdown.jbm.JBM import chat.revolt.components.markdown.jbm.JBM
import chat.revolt.components.markdown.jbm.JBMRenderer import chat.revolt.components.markdown.jbm.JBMRenderer
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
import chat.revolt.settings.dsl.SettingsPage import chat.revolt.settings.dsl.SettingsPage
@OptIn(JBM::class) @OptIn(JBM::class)
@ -21,6 +29,7 @@ import chat.revolt.settings.dsl.SettingsPage
fun JBMSandbox(navController: NavController) { fun JBMSandbox(navController: NavController) {
var mdSource by remember { mutableStateOf("") } var mdSource by remember { mutableStateOf("") }
var submitMdSource by remember { mutableStateOf<String?>(null) } var submitMdSource by remember { mutableStateOf<String?>(null) }
var isEmbedded by remember { mutableStateOf(false) }
SettingsPage( SettingsPage(
navController = navController, navController = navController,
@ -32,6 +41,20 @@ fun JBMSandbox(navController: NavController) {
) )
} }
) { ) {
Subcategory(
title = { Text("Options", maxLines = 1, overflow = TextOverflow.Ellipsis) },
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = isEmbedded,
onCheckedChange = { isEmbedded = it }
)
Spacer(Modifier.width(8.dp))
Text("Embedded", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
Subcategory( Subcategory(
title = { Text("Source", maxLines = 1, overflow = TextOverflow.Ellipsis) }, title = { Text("Source", maxLines = 1, overflow = TextOverflow.Ellipsis) },
) { ) {
@ -47,12 +70,43 @@ fun JBMSandbox(navController: NavController) {
}) { }) {
Text("Submit") Text("Submit")
} }
TextButton(onClick = {
submitMdSource = """# Full range of MD now supported!
1. Text with **bold**, *italics*, and ***both***!
2. You ~~can't see me~~.
3. [I'm a link to another website.](<https://revolt.chat>)
4. I'm a spoiler with ||**bold text inside it**||
- I'm a sub-item on this list...
- Let's go even deeper...
`Inline code`
```js
let x = "I'm a multi-line code block!";
```
> > ${'$'}${'$'}E = mc^2${'$'}${'$'}
>
> Albert Einstein
| Timestamp | Mention | Channel Link | Message Link |
|:-:|:-:|:-:|:-:|
| <t:1663846662:f> | <@01EX2NCWQ0CHS3QJF0FEQS1GR4> | <#01H73F4RAHTPBHKJ1XBQDXK3NQ> | https://revolt.chat/server/01F7ZSBSFHQ8TA81725KQCSDDP/channel/01F92C5ZXBQWQ8KY7J8KY917NM/01J25XZM9JXVVJDDKFPB7Q48HZ |"""
}) {
Text("Submit test document")
}
} }
Subcategory( Subcategory(
title = { Text("Output", maxLines = 1, overflow = TextOverflow.Ellipsis) }, title = { Text("Output", maxLines = 1, overflow = TextOverflow.Ellipsis) },
) { ) {
submitMdSource?.let { JBMRenderer(it, Modifier) } CompositionLocalProvider(
?: Text("Submit some Markdown and see the output.") LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
embedded = isEmbedded
)
) {
submitMdSource?.let { JBMRenderer(it, Modifier) }
?: Text("Submit some Markdown and see the output.")
}
} }
} }
} }

View File

@ -172,3 +172,14 @@ object ClearRippleTheme : RippleTheme {
pressedAlpha = 0.0f, pressedAlpha = 0.0f,
) )
} }
fun isThemeDark(theme: Theme, systemIsDark: Boolean): Boolean {
return when (theme) {
Theme.Revolt, Theme.Amoled -> true
Theme.Light -> false
Theme.M3Dynamic, Theme.None -> systemIsDark
}
}
@Composable
fun isThemeDark(theme: Theme) = isThemeDark(theme, isSystemInDarkTheme())