parent
087e4fe969
commit
fcbd13331c
|
|
@ -1,7 +1,11 @@
|
||||||
package chat.revolt.components.markdown.jbm
|
package chat.revolt.components.markdown.jbm
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.util.Log
|
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.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.horizontalScroll
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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
|
||||||
|
|
@ -25,17 +30,21 @@ import androidx.compose.runtime.compositionLocalOf
|
||||||
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.rememberCoroutineScope
|
||||||
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.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
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.graphics.toArgb
|
||||||
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.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
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.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 androidx.core.net.toUri
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
|
import chat.revolt.activities.InviteActivity
|
||||||
|
import chat.revolt.api.schemas.isInviteUri
|
||||||
import chat.revolt.api.settings.LoadedSettings
|
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.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.FragmentMono
|
||||||
|
|
@ -62,6 +76,7 @@ import dev.snipme.highlights.model.CodeHighlight
|
||||||
import dev.snipme.highlights.model.ColorHighlight
|
import dev.snipme.highlights.model.ColorHighlight
|
||||||
import dev.snipme.highlights.model.SyntaxLanguage
|
import dev.snipme.highlights.model.SyntaxLanguage
|
||||||
import dev.snipme.highlights.model.SyntaxThemes
|
import dev.snipme.highlights.model.SyntaxThemes
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
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
|
||||||
|
|
@ -69,6 +84,19 @@ import org.intellij.markdown.ast.getTextInNode
|
||||||
import org.intellij.markdown.flavours.gfm.GFMElementTypes
|
import org.intellij.markdown.flavours.gfm.GFMElementTypes
|
||||||
import org.intellij.markdown.flavours.gfm.GFMTokenTypes
|
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(
|
data class JBMarkdownTreeState(
|
||||||
val sourceText: String = "",
|
val sourceText: String = "",
|
||||||
val listDepth: Int = 0,
|
val listDepth: Int = 0,
|
||||||
|
|
@ -76,6 +104,10 @@ data class JBMarkdownTreeState(
|
||||||
val linksClickable: Boolean = true,
|
val linksClickable: Boolean = true,
|
||||||
val currentServer: String? = null,
|
val currentServer: String? = null,
|
||||||
val embedded: Boolean = false,
|
val embedded: Boolean = false,
|
||||||
|
val colors: JBMColors = JBMColors(
|
||||||
|
clickable = Color(0xFFFF00FF),
|
||||||
|
clickableBackground = Color(0x2000FF00)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val LocalJBMarkdownTreeState =
|
val LocalJBMarkdownTreeState =
|
||||||
|
|
@ -92,7 +124,11 @@ fun JBMRenderer(content: String, modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
|
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) {
|
if (LocalJBMarkdownTreeState.current.embedded) {
|
||||||
|
|
@ -214,13 +250,55 @@ private fun annotateText(
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkdownElementTypes.PARAGRAPH,
|
MarkdownElementTypes.PARAGRAPH,
|
||||||
MarkdownElementTypes.HTML_BLOCK,
|
MarkdownElementTypes.HTML_BLOCK -> {
|
||||||
MarkdownTokenTypes.ATX_CONTENT -> {
|
|
||||||
for (child in node.children) {
|
for (child in node.children) {
|
||||||
append(annotateText(state, child))
|
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
|
// re-render types
|
||||||
// for example, various syntactic elements like exclamation marks, brackets, etc.
|
// for example, various syntactic elements like exclamation marks, brackets, etc.
|
||||||
// we simply append the text as is
|
// we simply append the text as is
|
||||||
|
|
@ -238,14 +316,27 @@ private fun annotateText(
|
||||||
MarkdownTokenTypes.WHITE_SPACE,
|
MarkdownTokenTypes.WHITE_SPACE,
|
||||||
MarkdownTokenTypes.COLON,
|
MarkdownTokenTypes.COLON,
|
||||||
MarkdownTokenTypes.EMPH,
|
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))
|
append(node.getTextInNode(sourceText))
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
append("[${node.type.name}]{\n")
|
withStyle(SpanStyle(color = Color.Cyan)) {
|
||||||
append(node.getTextInNode(sourceText))
|
append("[${node.type.name}]{\n")
|
||||||
append("\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 mdState = LocalJBMarkdownTreeState.current
|
||||||
val annotatedText = remember(node) { annotateText(mdState, node) }
|
val annotatedText = remember(node) { annotateText(mdState, node) }
|
||||||
val colours = MaterialTheme.colorScheme
|
val colours = MaterialTheme.colorScheme
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val shouldConsumeTap = handler@{ offset: Int ->
|
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(
|
if (annotatedText.getStringAnnotations(
|
||||||
tag = tag,
|
tag = tag,
|
||||||
start = offset,
|
start = offset,
|
||||||
|
|
@ -284,12 +381,76 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
|
||||||
|
|
||||||
val onClick = handler@{ offset: Int ->
|
val onClick = handler@{ offset: Int ->
|
||||||
if (mdState.linksClickable) {
|
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 ->
|
val onLongClick = handler@{ offset: Int ->
|
||||||
if (mdState.linksClickable) {
|
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(
|
Text(
|
||||||
|
|
@ -495,12 +656,15 @@ private fun JBMCodeBlockContent(node: ASTNode, modifier: Modifier) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JBMBlock(node: ASTNode, modifier: Modifier) {
|
private fun JBMBlock(node: ASTNode, modifier: Modifier, nestingCounter: Int = 0) {
|
||||||
val state = LocalJBMarkdownTreeState.current
|
val state = LocalJBMarkdownTreeState.current
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
MarkdownElementTypes.PARAGRAPH,
|
MarkdownElementTypes.PARAGRAPH,
|
||||||
MarkdownElementTypes.HTML_BLOCK -> {
|
MarkdownElementTypes.HTML_BLOCK,
|
||||||
|
MarkdownElementTypes.LINK_DEFINITION,
|
||||||
|
MarkdownTokenTypes.WHITE_SPACE -> {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||||
fontSize = LocalTextStyle.current.fontSize * state.fontSizeMultiplier
|
fontSize = LocalTextStyle.current.fontSize * state.fontSizeMultiplier
|
||||||
|
|
@ -557,6 +721,49 @@ private fun JBMBlock(node: ASTNode, modifier: Modifier) {
|
||||||
JBMCodeBlockContent(node, 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 -> {
|
else -> {
|
||||||
Text(
|
Text(
|
||||||
text = buildAnnotatedString {
|
text = buildAnnotatedString {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue