chore: remove outdated markdown composables

This commit is contained in:
infi 2026-06-06 06:55:51 +02:00
parent 1c88229d4e
commit a44093c356
3 changed files with 0 additions and 640 deletions

View File

@ -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,
)
}
}

View File

@ -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)
}
)
}
)
}

View File

@ -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)
}
}
}