feat(jbm): more jbm

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-10-21 03:56:28 +02:00
parent 087e4fe969
commit fcbd13331c
1 changed files with 217 additions and 10 deletions

View File

@ -1,7 +1,11 @@
package chat.revolt.components.markdown.jbm
import android.content.Intent
import android.content.res.Configuration
import android.util.Log
import android.widget.Toast
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
@ -9,6 +13,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -25,17 +30,21 @@ import androidx.compose.runtime.compositionLocalOf
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.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@ -50,8 +59,13 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import chat.revolt.R
import chat.revolt.activities.InviteActivity
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.components.markdown.Annotations
import chat.revolt.components.utils.detectTapGesturesConditionalConsume
import chat.revolt.ui.theme.FragmentMono
@ -62,6 +76,7 @@ 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 kotlinx.coroutines.launch
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
@ -69,6 +84,19 @@ import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.flavours.gfm.GFMElementTypes
import org.intellij.markdown.flavours.gfm.GFMTokenTypes
enum class JBMAnnotations(val tag: String, val clickable: Boolean) {
URL("URL", true),
UserMention("UserMention", true),
ChannelMention("ChannelMention", true),
CustomEmote("CustomEmote", true),
Timestamp("Timestamp", false)
}
data class JBMColors(
val clickable: Color,
val clickableBackground: Color,
)
data class JBMarkdownTreeState(
val sourceText: String = "",
val listDepth: Int = 0,
@ -76,6 +104,10 @@ data class JBMarkdownTreeState(
val linksClickable: Boolean = true,
val currentServer: String? = null,
val embedded: Boolean = false,
val colors: JBMColors = JBMColors(
clickable = Color(0xFFFF00FF),
clickableBackground = Color(0x2000FF00)
)
)
val LocalJBMarkdownTreeState =
@ -92,7 +124,11 @@ fun JBMRenderer(content: String, modifier: Modifier = Modifier) {
CompositionLocalProvider(
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
sourceText = content
sourceText = content,
colors = JBMColors(
clickable = MaterialTheme.colorScheme.primary,
clickableBackground = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
)
)
) {
if (LocalJBMarkdownTreeState.current.embedded) {
@ -214,13 +250,55 @@ private fun annotateText(
}
MarkdownElementTypes.PARAGRAPH,
MarkdownElementTypes.HTML_BLOCK,
MarkdownTokenTypes.ATX_CONTENT -> {
MarkdownElementTypes.HTML_BLOCK -> {
for (child in node.children) {
append(annotateText(state, child))
}
}
MarkdownTokenTypes.ATX_CONTENT -> {
// Drop WHITE_SPACE children at the start
for (child in node.children.dropWhile { it.type == MarkdownTokenTypes.WHITE_SPACE }) {
append(annotateText(state, child))
}
}
MarkdownElementTypes.INLINE_LINK -> {
val linkTextChild =
node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT }
val linkDestinationChild =
node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_DESTINATION }
?: node.children.firstOrNull { it.type == MarkdownElementTypes.AUTOLINK }
pushStringAnnotation(
tag = JBMAnnotations.URL.tag,
annotation = linkDestinationChild?.getTextInNode(sourceText).toString()
.removeSurrounding("<", ">")
)
pushStyle(SpanStyle(color = state.colors.clickable))
linkTextChild?.children
?.drop(1) // l-bracket
?.dropLast(1) // r-bracket
?.forEach {
append(annotateText(state, it))
}
pop()
pop()
}
GFMTokenTypes.GFM_AUTOLINK,
MarkdownTokenTypes.AUTOLINK -> {
pushStringAnnotation(
tag = JBMAnnotations.URL.tag,
annotation = node.getTextInNode(sourceText).toString()
.removeSurrounding("<", ">")
)
pushStyle(SpanStyle(color = state.colors.clickable))
append(node.getTextInNode(sourceText))
pop()
pop()
}
// re-render types
// for example, various syntactic elements like exclamation marks, brackets, etc.
// we simply append the text as is
@ -238,14 +316,27 @@ private fun annotateText(
MarkdownTokenTypes.WHITE_SPACE,
MarkdownTokenTypes.COLON,
MarkdownTokenTypes.EMPH,
GFMTokenTypes.TILDE -> {
GFMTokenTypes.TILDE,
GFMTokenTypes.DOLLAR -> {
append(node.getTextInNode(sourceText))
}
MarkdownElementTypes.SHORT_REFERENCE_LINK,
MarkdownElementTypes.LINK_DEFINITION,
MarkdownElementTypes.FULL_REFERENCE_LINK -> {
append(node.getTextInNode(sourceText))
}
else -> {
append("[${node.type.name}]{\n")
append(node.getTextInNode(sourceText))
append("\n}")
withStyle(SpanStyle(color = Color.Cyan)) {
append("[${node.type.name}]{\n")
}
for (child in node.children) {
append(annotateText(state, child))
}
withStyle(SpanStyle(color = Color.Cyan)) {
append("\n}")
}
}
}
}
@ -266,9 +357,15 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
val mdState = LocalJBMarkdownTreeState.current
val annotatedText = remember(node) { annotateText(mdState, node) }
val colours = MaterialTheme.colorScheme
val scope = rememberCoroutineScope()
val context = LocalContext.current
val shouldConsumeTap = handler@{ offset: Int ->
Annotations.entries.filter { it.clickable }.map { it.tag }.forEach { tag ->
if (!mdState.linksClickable) {
return@handler false
}
JBMAnnotations.entries.filter { it.clickable }.map { it.tag }.forEach { tag ->
if (annotatedText.getStringAnnotations(
tag = tag,
start = offset,
@ -284,12 +381,76 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
val onClick = handler@{ offset: Int ->
if (mdState.linksClickable) {
JBMAnnotations.entries.filter { it.clickable }.map { it.tag }.forEach { tag ->
val annotations = annotatedText.getStringAnnotations(
tag = tag,
start = offset,
end = offset
)
annotations.forEach { annotation ->
val item = annotation.item
when (tag) {
JBMAnnotations.URL.tag -> {
try {
val uri = item.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(colours.surfaceContainer.toArgb())
.build()
)
.build()
try {
customTab.launchUrl(context, item.toUri())
} catch (e: Exception) {
Toast.makeText(
context,
context.getString(R.string.link_type_no_intent),
Toast.LENGTH_SHORT
).show()
}
return@handler true
}
}
}
}
}
return@handler false
}
val onLongClick = handler@{ offset: Int ->
if (mdState.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
}
}
return@handler false
}
Text(
@ -495,12 +656,15 @@ private fun JBMCodeBlockContent(node: ASTNode, modifier: Modifier) {
}
@Composable
private fun JBMBlock(node: ASTNode, modifier: Modifier) {
private fun JBMBlock(node: ASTNode, modifier: Modifier, nestingCounter: Int = 0) {
val state = LocalJBMarkdownTreeState.current
val colorScheme = MaterialTheme.colorScheme
when (node.type) {
MarkdownElementTypes.PARAGRAPH,
MarkdownElementTypes.HTML_BLOCK -> {
MarkdownElementTypes.HTML_BLOCK,
MarkdownElementTypes.LINK_DEFINITION,
MarkdownTokenTypes.WHITE_SPACE -> {
CompositionLocalProvider(
LocalTextStyle provides LocalTextStyle.current.copy(
fontSize = LocalTextStyle.current.fontSize * state.fontSizeMultiplier
@ -557,6 +721,49 @@ private fun JBMBlock(node: ASTNode, modifier: Modifier) {
JBMCodeBlockContent(node, modifier)
}
MarkdownElementTypes.BLOCK_QUOTE -> {
if (LocalJBMarkdownTreeState.current.embedded) {
node.children.getOrNull(0)?.let {
JBMBlock(it, modifier)
}
} else {
Column(
Modifier
.clip(MaterialTheme.shapes.medium)
.fillMaxWidth()
.drawBehind {
drawRect(colorScheme.surfaceContainer.copy(alpha = 0.5f))
drawLine(
colorScheme.primary,
Offset.Zero,
Offset(0f, size.height),
strokeWidth = 16f
)
}
.padding(8.dp)
.padding(start = 4.dp)
) {
if (nestingCounter < 5) {
node.children.map {
JBMBlock(it, modifier, nestingCounter = nestingCounter + 1)
}
}
}
}
}
MarkdownTokenTypes.BLOCK_QUOTE -> {
if (LocalJBMarkdownTreeState.current.embedded) {
node.children.getOrNull(0)?.let {
JBMBlock(it, modifier)
}
} else {
node.children.map {
JBMBlock(it, modifier)
}
}
}
else -> {
Text(
text = buildAnnotatedString {