chore: remove outdated markdown composables
This commit is contained in:
parent
1c88229d4e
commit
a44093c356
|
|
@ -1,35 +0,0 @@
|
||||||
package chat.stoat.composables.markdown
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
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.unit.dp
|
|
||||||
import chat.stoat.ndk.AstNode
|
|
||||||
import chat.stoat.ui.theme.FragmentMono
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MarkdownCodeBlock(node: AstNode, modifier: Modifier = Modifier) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.then(
|
|
||||||
if (node.startLine != 1) {
|
|
||||||
Modifier.padding(top = 8.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.clip(MaterialTheme.shapes.medium)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = node.text?.removeSuffix("\n") ?: "",
|
|
||||||
fontFamily = FragmentMono,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,471 +0,0 @@
|
||||||
package chat.stoat.composables.markdown
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.Placeholder
|
|
||||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontSynthesis
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import chat.stoat.R
|
|
||||||
import chat.stoat.activities.InviteActivity
|
|
||||||
import chat.stoat.api.StoatAPI
|
|
||||||
import chat.stoat.api.routes.custom.fetchEmoji
|
|
||||||
import chat.stoat.core.model.schemas.isInviteUri
|
|
||||||
import chat.stoat.callbacks.Action
|
|
||||||
import chat.stoat.callbacks.ActionChannel
|
|
||||||
import chat.stoat.composables.generic.RemoteImage
|
|
||||||
import chat.stoat.composables.utils.detectTapGesturesConditionalConsume
|
|
||||||
import chat.stoat.core.model.data.STOAT_FILES
|
|
||||||
import chat.stoat.internals.resolveTimestamp
|
|
||||||
import chat.stoat.ndk.AstNode
|
|
||||||
import chat.stoat.ui.theme.FragmentMono
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
enum class Annotations(val tag: String, val clickable: Boolean) {
|
|
||||||
URL("URL", true),
|
|
||||||
UserMention("UserMention", true),
|
|
||||||
ChannelMention("ChannelMention", true),
|
|
||||||
CustomEmote("CustomEmote", true),
|
|
||||||
Timestamp("Timestamp", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
object MarkdownTextRegularExpressions {
|
|
||||||
val Mention = Regex("<@([0-9A-Z]{26})>")
|
|
||||||
val Channel = Regex("<#([0-9A-Z]{26})>")
|
|
||||||
val CustomEmote = Regex(":([0-9A-Z]{26}):")
|
|
||||||
val Timestamp = Regex("<t:([0-9]+?)(:[tTDfFR])?>")
|
|
||||||
val UrlFallback =
|
|
||||||
Regex("<?https?://(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_+.~#?&/=]*)>?")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit the AST and its children and return an [AnnotatedString] with the appropriate annotations.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun annotateText(node: AstNode): AnnotatedString {
|
|
||||||
return buildAnnotatedString {
|
|
||||||
when (node.stringType) {
|
|
||||||
"text" -> {
|
|
||||||
val text = node.text ?: ""
|
|
||||||
|
|
||||||
val mentions = MarkdownTextRegularExpressions.Mention.findAll(text)
|
|
||||||
val channels = MarkdownTextRegularExpressions.Channel.findAll(text)
|
|
||||||
val customEmotes = MarkdownTextRegularExpressions.CustomEmote.findAll(text)
|
|
||||||
val timestamps = MarkdownTextRegularExpressions.Timestamp.findAll(text)
|
|
||||||
val urls = MarkdownTextRegularExpressions.UrlFallback.findAll(text)
|
|
||||||
|
|
||||||
var lastIndex = 0
|
|
||||||
for (mention in mentions) {
|
|
||||||
try {
|
|
||||||
append(text.substring(lastIndex, mention.range.first))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = Annotations.UserMention.tag,
|
|
||||||
annotation = mention.groupValues[1]
|
|
||||||
)
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
background = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val member = LocalMarkdownTreeConfig.current.currentServer?.let { serverId ->
|
|
||||||
StoatAPI.members.getMember(serverId, mention.groupValues[1])
|
|
||||||
}
|
|
||||||
val content = member?.nickname?.let { nick -> "@$nick" }
|
|
||||||
?: StoatAPI.userCache[mention.groupValues[1]]?.username?.let { username -> "@$username" }
|
|
||||||
?: "<@${mention.groupValues[1]}>"
|
|
||||||
append(content)
|
|
||||||
pop()
|
|
||||||
pop()
|
|
||||||
lastIndex = mention.range.last + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
for (channel in channels) {
|
|
||||||
try {
|
|
||||||
append(text.substring(lastIndex, channel.range.first))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = Annotations.ChannelMention.tag,
|
|
||||||
annotation = channel.groupValues[1]
|
|
||||||
)
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
background = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val content =
|
|
||||||
StoatAPI.channelCache[channel.groupValues[1]]?.name?.let { chId -> "#$chId" }
|
|
||||||
?: "<#${channel.groupValues[1]}>"
|
|
||||||
append(content)
|
|
||||||
pop()
|
|
||||||
pop()
|
|
||||||
lastIndex = channel.range.last + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
for (emote in customEmotes) {
|
|
||||||
try {
|
|
||||||
append(text.substring(lastIndex, emote.range.first))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = Annotations.CustomEmote.tag,
|
|
||||||
annotation = emote.groupValues[1]
|
|
||||||
)
|
|
||||||
appendInlineContent("CustomEmote", emote.groupValues[1])
|
|
||||||
pop()
|
|
||||||
lastIndex = emote.range.last + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
for (timestamp in timestamps) {
|
|
||||||
try {
|
|
||||||
append(text.substring(lastIndex, timestamp.range.first))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = Annotations.Timestamp.tag,
|
|
||||||
annotation = timestamp.groupValues[1]
|
|
||||||
)
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
fontFamily = FragmentMono,
|
|
||||||
background = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
append(
|
|
||||||
resolveTimestamp(
|
|
||||||
try {
|
|
||||||
timestamp.groupValues[1].toLong()
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
-1
|
|
||||||
},
|
|
||||||
timestamp.groupValues.getOrNull(2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pop()
|
|
||||||
lastIndex = timestamp.range.last + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yes, cmark should handle this, but for gTLDs like .chat it doesn't.
|
|
||||||
// As a service with a .chat TLD, this is a problem. Duct tape fix, their fault.
|
|
||||||
for (url in urls) {
|
|
||||||
try {
|
|
||||||
append(text.substring(lastIndex, url.range.first))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = Annotations.URL.tag,
|
|
||||||
annotation = url.value
|
|
||||||
)
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
append(url.value)
|
|
||||||
pop()
|
|
||||||
pop()
|
|
||||||
lastIndex = url.range.last + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
append(text.substring(lastIndex, text.length))
|
|
||||||
}
|
|
||||||
|
|
||||||
"emph" -> {
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
fontSynthesis = FontSynthesis.All
|
|
||||||
)
|
|
||||||
)
|
|
||||||
node.children?.forEach { append(annotateText(it)) }
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
"strong" -> {
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSynthesis = FontSynthesis.All
|
|
||||||
)
|
|
||||||
)
|
|
||||||
node.children?.forEach { append(annotateText(it)) }
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
"link" -> {
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = Annotations.URL.tag,
|
|
||||||
annotation = node.url ?: ""
|
|
||||||
)
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
node.children?.forEach { append(annotateText(it)) }
|
|
||||||
pop()
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
"code" -> {
|
|
||||||
pushStyle(
|
|
||||||
LocalTextStyle.current.toSpanStyle()
|
|
||||||
.copy(
|
|
||||||
fontFamily = FragmentMono,
|
|
||||||
fontSynthesis = FontSynthesis.All,
|
|
||||||
background = MaterialTheme.colorScheme.surfaceContainer
|
|
||||||
)
|
|
||||||
)
|
|
||||||
append(node.text ?: "")
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
"softbreak" -> {
|
|
||||||
append("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
node.children?.forEach { append(annotateText(it)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MarkdownText(textNode: AstNode, modifier: Modifier = Modifier) {
|
|
||||||
var layoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
|
||||||
val annotatedText = annotateText(textNode)
|
|
||||||
val context = LocalContext.current
|
|
||||||
val background = MaterialTheme.colorScheme.background
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val markdownConfig = LocalMarkdownTreeConfig.current
|
|
||||||
|
|
||||||
val shouldConsumeTap = handler@{ offset: Int ->
|
|
||||||
Annotations.entries.filter { it.clickable }.map { it.tag }.forEach { tag ->
|
|
||||||
if (annotatedText.getStringAnnotations(
|
|
||||||
tag = tag,
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
).isNotEmpty()
|
|
||||||
) {
|
|
||||||
return@handler true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return@handler false
|
|
||||||
}
|
|
||||||
|
|
||||||
val onClick = handler@{ offset: Int ->
|
|
||||||
if (markdownConfig.linksClickable) {
|
|
||||||
annotatedText.getStringAnnotations(
|
|
||||||
tag = Annotations.URL.tag,
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
).firstOrNull()?.let { annotation ->
|
|
||||||
val url = annotation.item
|
|
||||||
|
|
||||||
try {
|
|
||||||
val uri = url.toUri()
|
|
||||||
if (uri.isInviteUri()) {
|
|
||||||
scope.launch {
|
|
||||||
Intent(context, InviteActivity::class.java).apply {
|
|
||||||
data = uri
|
|
||||||
context.startActivity(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return@handler true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
val customTab = CustomTabsIntent.Builder()
|
|
||||||
.setShowTitle(true)
|
|
||||||
.setDefaultColorSchemeParams(
|
|
||||||
CustomTabColorSchemeParams.Builder()
|
|
||||||
.setToolbarColor(background.toArgb())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
try {
|
|
||||||
customTab.launchUrl(context, url.toUri())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.link_type_no_intent),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
return@handler true
|
|
||||||
}
|
|
||||||
|
|
||||||
annotatedText.getStringAnnotations(
|
|
||||||
tag = Annotations.UserMention.tag,
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
).firstOrNull()?.let { annotation ->
|
|
||||||
scope.launch {
|
|
||||||
ActionChannel.send(
|
|
||||||
Action.OpenUserSheet(
|
|
||||||
annotation.item,
|
|
||||||
markdownConfig.currentServer
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return@handler true
|
|
||||||
}
|
|
||||||
|
|
||||||
annotatedText.getStringAnnotations(
|
|
||||||
tag = Annotations.ChannelMention.tag,
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
).firstOrNull()?.let { annotation ->
|
|
||||||
scope.launch {
|
|
||||||
ActionChannel.send(Action.SwitchChannel(annotation.item))
|
|
||||||
}
|
|
||||||
|
|
||||||
return@handler true
|
|
||||||
}
|
|
||||||
|
|
||||||
annotatedText.getStringAnnotations(
|
|
||||||
tag = Annotations.CustomEmote.tag,
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
).firstOrNull()?.let { annotation ->
|
|
||||||
scope.launch {
|
|
||||||
ActionChannel.send(Action.EmoteInfo(annotation.item))
|
|
||||||
}
|
|
||||||
|
|
||||||
return@handler true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
val onLongClick = handler@{ offset: Int ->
|
|
||||||
if (markdownConfig.linksClickable) {
|
|
||||||
annotatedText.getStringAnnotations(
|
|
||||||
tag = Annotations.URL.tag,
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
).firstOrNull()?.let { annotation ->
|
|
||||||
scope.launch {
|
|
||||||
ActionChannel.send(Action.LinkInfo(annotation.item))
|
|
||||||
}
|
|
||||||
|
|
||||||
return@handler true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
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 = StoatAPI.emojiCache[id]
|
|
||||||
if (emote == null) {
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
StoatAPI.emojiCache[id] = fetchEmoji(id)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return@InlineTextContent
|
|
||||||
} else {
|
|
||||||
with(LocalDensity.current) {
|
|
||||||
RemoteImage(
|
|
||||||
url = "$STOAT_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 ->
|
|
||||||
val index =
|
|
||||||
layoutResult?.getOffsetForPosition(pos)
|
|
||||||
?: return@detectTapGesturesConditionalConsume
|
|
||||||
onClick(index)
|
|
||||||
},
|
|
||||||
onLongPress = { pos ->
|
|
||||||
val index =
|
|
||||||
layoutResult?.getOffsetForPosition(pos)
|
|
||||||
?: return@detectTapGesturesConditionalConsume
|
|
||||||
onLongClick(index)
|
|
||||||
},
|
|
||||||
shouldConsumeTap = { pos ->
|
|
||||||
val index =
|
|
||||||
layoutResult?.getOffsetForPosition(pos)
|
|
||||||
?: return@detectTapGesturesConditionalConsume false
|
|
||||||
shouldConsumeTap(index)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
package chat.stoat.composables.markdown
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
|
||||||
import androidx.compose.runtime.structuralEqualityPolicy
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.stoat.api.internals.solidColor
|
|
||||||
import chat.stoat.ndk.AstNode
|
|
||||||
|
|
||||||
data class MarkdownTreeConfig(
|
|
||||||
val linksClickable: Boolean = true,
|
|
||||||
val currentServer: String? = null,
|
|
||||||
val fontSizeMultiplier: Float = 1f
|
|
||||||
)
|
|
||||||
|
|
||||||
val LocalMarkdownTreeConfig =
|
|
||||||
compositionLocalOf(structuralEqualityPolicy()) { MarkdownTreeConfig() }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Children(node: AstNode) {
|
|
||||||
node.children?.forEach { MarkdownTree(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MarkdownTree(node: AstNode) {
|
|
||||||
when (node.stringType) {
|
|
||||||
"heading" -> {
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = when (node.level) {
|
|
||||||
1 -> 32.sp * LocalMarkdownTreeConfig.current.fontSizeMultiplier
|
|
||||||
2 -> 24.sp * LocalMarkdownTreeConfig.current.fontSizeMultiplier
|
|
||||||
3 -> 20.sp * LocalMarkdownTreeConfig.current.fontSizeMultiplier
|
|
||||||
4 -> 16.sp * LocalMarkdownTreeConfig.current.fontSizeMultiplier
|
|
||||||
5 -> 14.sp * LocalMarkdownTreeConfig.current.fontSizeMultiplier
|
|
||||||
else -> 12.sp * LocalMarkdownTreeConfig.current.fontSizeMultiplier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (node.startLine != 1) {
|
|
||||||
Box(Modifier.padding(top = 8.dp)) {
|
|
||||||
MarkdownText(node)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MarkdownText(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"paragraph" -> {
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
|
||||||
fontSize = LocalTextStyle.current.fontSize * LocalMarkdownTreeConfig.current.fontSizeMultiplier
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
MarkdownText(
|
|
||||||
node,
|
|
||||||
modifier = Modifier
|
|
||||||
.then(
|
|
||||||
if (node.startLine != 1) {
|
|
||||||
Modifier.padding(top = 8.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"document" -> {
|
|
||||||
Children(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
"text" -> {
|
|
||||||
MarkdownText(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
"code_block" -> {
|
|
||||||
MarkdownCodeBlock(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
"block_quote" -> {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 4.dp, bottom = 4.dp)
|
|
||||||
.clip(MaterialTheme.shapes.medium)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
) {
|
|
||||||
// Stripe at the left side of the blockquote
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(4.dp)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.background(
|
|
||||||
Brush.solidColor(MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Children(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Children(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue