Continue working on html rendering
This commit is contained in:
parent
20fcf51e1e
commit
42ecee8d52
4 changed files with 319 additions and 277 deletions
|
|
@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.x.features.messages.html.HtmlDocument
|
import io.element.android.x.features.messages.components.html.HtmlDocument
|
||||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -12,10 +12,11 @@ fun MessagesTimelineItemTextView(
|
||||||
content: MessagesTimelineItemTextBasedContent,
|
content: MessagesTimelineItemTextBasedContent,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Box(modifier) {
|
val htmlDocument = content.htmlDocument
|
||||||
if (content.htmlDocument != null) {
|
if (htmlDocument != null) {
|
||||||
HtmlDocument(document = content.htmlDocument!!)
|
HtmlDocument(document = htmlDocument, modifier)
|
||||||
} else {
|
} else {
|
||||||
|
Box(modifier) {
|
||||||
Text(text = content.body)
|
Text(text = content.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
package io.element.android.x.features.messages.components.html
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.nodes.Node
|
||||||
|
import org.jsoup.nodes.TextNode
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HtmlDocument(document: Document, modifier: Modifier = Modifier) {
|
||||||
|
HtmlBody(body = document.body(), modifier = modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlBody(body: Element, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
for (node in body.childNodes()) {
|
||||||
|
when (node) {
|
||||||
|
is TextNode -> {
|
||||||
|
if (!node.isBlank) {
|
||||||
|
Text(text = node.text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Element -> {
|
||||||
|
HtmlBlock(element = node)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlBlock(element: Element, modifier: Modifier = Modifier) {
|
||||||
|
val blockModifier = modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
when (element.normalName()) {
|
||||||
|
"p" -> HtmlParagraph(element, blockModifier)
|
||||||
|
"h1", "h2", "h3", "h4", "h5", "h6" -> HtmlHeading(element, blockModifier)
|
||||||
|
"ol" -> HtmlOrderedList(element, blockModifier)
|
||||||
|
"ul" -> HtmlUnorderedList(element, blockModifier)
|
||||||
|
"blockquote" -> HtmlBlockquote(element, blockModifier)
|
||||||
|
"pre" -> HtmlPreformatted(element, blockModifier)
|
||||||
|
"mx-reply" -> HtmlMxReply(element, blockModifier)
|
||||||
|
// fallback to html inline
|
||||||
|
else -> HtmlInline(element, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlInline(element: Element, modifier: Modifier = Modifier) {
|
||||||
|
Box(modifier.padding(start = 8.dp)) {
|
||||||
|
val styledText = buildAnnotatedString {
|
||||||
|
appendInlineElement(element, MaterialTheme.colorScheme)
|
||||||
|
}
|
||||||
|
Text(styledText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlPreformatted(pre: Element, modifier: Modifier = Modifier) {
|
||||||
|
val isCode = pre.firstElementChild()?.normalName() == "code"
|
||||||
|
val backgroundColor =
|
||||||
|
if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified
|
||||||
|
Box(modifier.background(color = backgroundColor)) {
|
||||||
|
Text(
|
||||||
|
text = pre.wholeText(),
|
||||||
|
style = TextStyle(fontFamily = FontFamily.Monospace),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlParagraph(paragraph: Element, modifier: Modifier = Modifier) {
|
||||||
|
Box(modifier) {
|
||||||
|
val styledText = buildAnnotatedString {
|
||||||
|
appendInlineChildrenElements(paragraph.childNodes(), MaterialTheme.colorScheme)
|
||||||
|
}
|
||||||
|
Text(styledText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlBlockquote(blockquote: Element, modifier: Modifier = Modifier) {
|
||||||
|
val color = MaterialTheme.colorScheme.onBackground
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.drawBehind {
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
strokeWidth = 2f,
|
||||||
|
start = Offset(12.dp.value, 0f),
|
||||||
|
end = Offset(12.dp.value, size.height)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(start = 8.dp, top = 4.dp, bottom = 4.dp)
|
||||||
|
) {
|
||||||
|
val text = buildAnnotatedString {
|
||||||
|
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
|
||||||
|
appendInlineChildrenElements(blockquote.childNodes(), MaterialTheme.colorScheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlHeading(heading: Element, modifier: Modifier = Modifier) {
|
||||||
|
val style = when (heading.normalName()) {
|
||||||
|
"h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp)
|
||||||
|
"h2" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 26.sp)
|
||||||
|
"h3" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 22.sp)
|
||||||
|
"h4" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 18.sp)
|
||||||
|
"h5" -> MaterialTheme.typography.headlineSmall.copy(fontSize = 14.sp)
|
||||||
|
"h6" -> MaterialTheme.typography.headlineSmall.copy(fontSize = 12.sp)
|
||||||
|
else -> {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(modifier) {
|
||||||
|
val text = buildAnnotatedString {
|
||||||
|
appendInlineChildrenElements(heading.childNodes(), MaterialTheme.colorScheme)
|
||||||
|
}
|
||||||
|
Text(text, style = style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlMxReply(mxReply: Element, modifier: Modifier = Modifier) {
|
||||||
|
val blockquote = mxReply.childNodes().firstOrNull() ?: return
|
||||||
|
val shape = RoundedCornerShape(12.dp)
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.offset(x = -(8.dp)),
|
||||||
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
shape = shape,
|
||||||
|
) {
|
||||||
|
val text = buildAnnotatedString {
|
||||||
|
for (blockquoteNode in blockquote.childNodes()) {
|
||||||
|
when (blockquoteNode) {
|
||||||
|
is TextNode -> {
|
||||||
|
withStyle(
|
||||||
|
style = SpanStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
append(blockquoteNode.text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Element -> {
|
||||||
|
when (blockquoteNode.normalName()) {
|
||||||
|
"br" -> {
|
||||||
|
append('\n')
|
||||||
|
}
|
||||||
|
"a" -> {
|
||||||
|
append(blockquoteNode.ownText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(text, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlOrderedList(unorderedList: Element, modifier: Modifier = Modifier) {
|
||||||
|
var number = 1
|
||||||
|
val delimiter = "."
|
||||||
|
HtmlListItems(unorderedList, modifier = modifier) {
|
||||||
|
val text = buildAnnotatedString {
|
||||||
|
append("${number++}$delimiter ${it.text()}")
|
||||||
|
}
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlUnorderedList(unorderedList: Element, modifier: Modifier = Modifier) {
|
||||||
|
val marker = "・"
|
||||||
|
HtmlListItems(unorderedList, modifier = modifier) {
|
||||||
|
val text = buildAnnotatedString {
|
||||||
|
append("$marker ${it.text()}")
|
||||||
|
}
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HtmlListItems(
|
||||||
|
list: Element,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable (node: TextNode) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
for (node in list.children()) {
|
||||||
|
for (innerNode in node.childNodes()) {
|
||||||
|
when (innerNode) {
|
||||||
|
is TextNode -> {
|
||||||
|
if (!innerNode.isBlank) content(innerNode)
|
||||||
|
}
|
||||||
|
is Element -> HtmlBlock(
|
||||||
|
element = innerNode,
|
||||||
|
modifier = modifier.padding(start = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ColorScheme.codeBackground(): Color {
|
||||||
|
return background.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AnnotatedString.Builder.appendInlineChildrenElements(
|
||||||
|
childNodes: List<Node>,
|
||||||
|
colors: ColorScheme
|
||||||
|
) {
|
||||||
|
|
||||||
|
for (node in childNodes) {
|
||||||
|
when (node) {
|
||||||
|
is TextNode -> {
|
||||||
|
append(node.text())
|
||||||
|
}
|
||||||
|
is Element -> {
|
||||||
|
appendInlineElement(node, colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) {
|
||||||
|
when (element.normalName()) {
|
||||||
|
"br" -> {
|
||||||
|
append('\n')
|
||||||
|
}
|
||||||
|
"code" -> {
|
||||||
|
withStyle(
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
background = colors.codeBackground()
|
||||||
|
).toSpanStyle()
|
||||||
|
) {
|
||||||
|
appendInlineChildrenElements(element.childNodes(), colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"del" -> {
|
||||||
|
withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
|
||||||
|
appendInlineChildrenElements(element.childNodes(), colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"em" -> {
|
||||||
|
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
|
||||||
|
appendInlineChildrenElements(element.childNodes(), colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"strong" -> {
|
||||||
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
appendInlineChildrenElements(element.childNodes(), colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"a" -> {
|
||||||
|
val href = element.attr("href")
|
||||||
|
pushStringAnnotation(tag = "url", annotation = href)
|
||||||
|
withStyle(
|
||||||
|
style = SpanStyle(
|
||||||
|
color = Color.Blue,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
append(element.ownText())
|
||||||
|
}
|
||||||
|
pop()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
appendInlineChildrenElements(element.childNodes(), colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
package io.element.android.x.features.messages.html
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.text.*
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil.compose.rememberImagePainter
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.jsoup.nodes.Node
|
|
||||||
import org.jsoup.nodes.TextNode
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HtmlDocument(document: Document, modifier: Modifier = Modifier) {
|
|
||||||
HtmlBody(body = document.body(), modifier = modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HtmlBody(body: Element, modifier: Modifier = Modifier) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
for (node in body.childNodes()) {
|
|
||||||
when (node) {
|
|
||||||
is TextNode -> {
|
|
||||||
if (!node.isBlank) {
|
|
||||||
Text(
|
|
||||||
text = node.text(),
|
|
||||||
style = MaterialTheme.typography.body1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Element -> {
|
|
||||||
HtmlBlock(node)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HtmlBlock(element: Element, modifier: Modifier = Modifier) {
|
|
||||||
val blockModifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 4.dp)
|
|
||||||
when (element.normalName()) {
|
|
||||||
"p" -> HtmlParagraph(element, blockModifier)
|
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6" -> HtmlHeading(heading = element, blockModifier)
|
|
||||||
"ol" -> HtmlOrderedList(element, blockModifier)
|
|
||||||
"ul" -> HtmlUnorderedList(element, blockModifier)
|
|
||||||
"blockquote" -> Column {
|
|
||||||
for (e in element.children()) {
|
|
||||||
HtmlBlock(element = e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HtmlHeading(heading: Element, modifier: Modifier = Modifier) {
|
|
||||||
val style = when (heading.normalName()) {
|
|
||||||
"h1" -> MaterialTheme.typography.h1
|
|
||||||
"h2" -> MaterialTheme.typography.h2
|
|
||||||
"h3" -> MaterialTheme.typography.h3
|
|
||||||
"h4" -> MaterialTheme.typography.h4
|
|
||||||
"h5" -> MaterialTheme.typography.h5
|
|
||||||
"h6" -> MaterialTheme.typography.h6
|
|
||||||
else -> {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(modifier) {
|
|
||||||
val text = buildAnnotatedString {
|
|
||||||
appendInlineChildrenElements(heading.childNodes())
|
|
||||||
}
|
|
||||||
HtmlText(text, style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HtmlOrderedList(unorderedList: Element, modifier: Modifier = Modifier) {
|
|
||||||
var number = 0
|
|
||||||
val delimiter = "."
|
|
||||||
HtmlListItems(unorderedList, modifier = modifier) {
|
|
||||||
val text = buildAnnotatedString {
|
|
||||||
pushStyle(MaterialTheme.typography.body1.toSpanStyle())
|
|
||||||
append("${number++}$delimiter ")
|
|
||||||
appendInlineElements(it)
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
HtmlText(text, MaterialTheme.typography.body1, modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HtmlUnorderedList(unorderedList: Element, modifier: Modifier = Modifier) {
|
|
||||||
val marker = "-"
|
|
||||||
HtmlListItems(unorderedList, modifier = modifier) {
|
|
||||||
val text = buildAnnotatedString {
|
|
||||||
pushStyle(MaterialTheme.typography.body1.toSpanStyle())
|
|
||||||
append("$marker ")
|
|
||||||
appendInlineElements(it)
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
HtmlText(text, MaterialTheme.typography.body1, modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HtmlListItems(
|
|
||||||
list: Element,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable (node: Element) -> Unit
|
|
||||||
) {
|
|
||||||
if (list.children().isEmpty()) return
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
val children = list.children().iterator()
|
|
||||||
var listItem = children.next()
|
|
||||||
while (listItem != null) {
|
|
||||||
val innerChildren = listItem.children().iterator()
|
|
||||||
var child = if (innerChildren.hasNext()) {
|
|
||||||
innerChildren.next()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
while (child != null) {
|
|
||||||
when (child.normalName()) {
|
|
||||||
"ul" -> HtmlUnorderedList(child, modifier)
|
|
||||||
"ol" -> HtmlOrderedList(child, modifier)
|
|
||||||
else -> content(child)
|
|
||||||
}
|
|
||||||
child = if (innerChildren.hasNext()) {
|
|
||||||
innerChildren.next()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listItem = if (children.hasNext()) {
|
|
||||||
children.next()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun AnnotatedString.Builder.appendInlineChildrenElements(childNodes: List<Node>) {
|
|
||||||
for (node in childNodes) {
|
|
||||||
when (node) {
|
|
||||||
is TextNode -> {
|
|
||||||
append(node.text())
|
|
||||||
}
|
|
||||||
is Element -> {
|
|
||||||
appendInlineElements(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun AnnotatedString.Builder.appendInlineElements(element: Element) {
|
|
||||||
when (element.normalName()) {
|
|
||||||
"br" -> {
|
|
||||||
append('\n')
|
|
||||||
}
|
|
||||||
"del" -> {
|
|
||||||
withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
|
|
||||||
appendInlineChildrenElements(element.childNodes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"em" -> {
|
|
||||||
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
|
|
||||||
appendInlineChildrenElements(element.childNodes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"strong" -> {
|
|
||||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
|
||||||
appendInlineChildrenElements(element.childNodes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"a" -> {
|
|
||||||
val href = element.attr("href")
|
|
||||||
pushStringAnnotation(tag = "url", annotation = href)
|
|
||||||
withStyle(
|
|
||||||
style = SpanStyle(
|
|
||||||
color = Color.Blue,
|
|
||||||
textDecoration = TextDecoration.Underline
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
append(element.ownText())
|
|
||||||
}
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HtmlText(text: AnnotatedString, style: TextStyle, modifier: Modifier = Modifier) {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
|
||||||
Text(text = text,
|
|
||||||
modifier.pointerInput(Unit) {
|
|
||||||
detectTapGestures { offset ->
|
|
||||||
layoutResult.value?.let { layoutResult ->
|
|
||||||
val position = layoutResult.getOffsetForPosition(offset)
|
|
||||||
text.getStringAnnotations(position, position)
|
|
||||||
.firstOrNull()
|
|
||||||
?.let { sa ->
|
|
||||||
if (sa.tag == "url") {
|
|
||||||
uriHandler.openUri(sa.item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = style,
|
|
||||||
inlineContent = mapOf(
|
|
||||||
"imageUrl" to InlineTextContent(
|
|
||||||
Placeholder(style.fontSize, style.fontSize, PlaceholderVerticalAlign.Bottom)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = rememberImagePainter(
|
|
||||||
data = it,
|
|
||||||
),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier,
|
|
||||||
alignment = Alignment.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
),
|
|
||||||
onTextLayout = { layoutResult.value = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HtmlParagraph(paragraph: Element, modifier: Modifier = Modifier) {
|
|
||||||
Box(modifier) {
|
|
||||||
val styledText = buildAnnotatedString {
|
|
||||||
pushStyle(MaterialTheme.typography.body1.toSpanStyle())
|
|
||||||
appendInlineChildrenElements(paragraph.childNodes())
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
HtmlText(styledText, MaterialTheme.typography.body1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,7 @@ class MessagesTimelineItemContentProvider : PreviewParameterProvider<MessagesTim
|
||||||
override val values = sequenceOf(
|
override val values = sequenceOf(
|
||||||
MessagesTimelineItemEmoteContent(
|
MessagesTimelineItemEmoteContent(
|
||||||
body = "Emote",
|
body = "Emote",
|
||||||
formattedBody = FormattedBody(MessageFormat.HTML, "Formatted emote")
|
htmlDocument = null
|
||||||
),
|
),
|
||||||
MessagesTimelineItemEncryptedContent(
|
MessagesTimelineItemEncryptedContent(
|
||||||
encryptedMessage = EncryptedMessage.Unknown
|
encryptedMessage = EncryptedMessage.Unknown
|
||||||
|
|
@ -19,12 +19,12 @@ class MessagesTimelineItemContentProvider : PreviewParameterProvider<MessagesTim
|
||||||
// TODO MessagesTimelineItemImageContent(),
|
// TODO MessagesTimelineItemImageContent(),
|
||||||
MessagesTimelineItemNoticeContent(
|
MessagesTimelineItemNoticeContent(
|
||||||
body = "Notice",
|
body = "Notice",
|
||||||
formattedBody = FormattedBody(MessageFormat.HTML, "Formatted notice")
|
htmlDocument = null
|
||||||
),
|
),
|
||||||
MessagesTimelineItemRedactedContent,
|
MessagesTimelineItemRedactedContent,
|
||||||
MessagesTimelineItemTextContent(
|
MessagesTimelineItemTextContent(
|
||||||
body = "Text",
|
body = "Text",
|
||||||
formattedBody = FormattedBody(MessageFormat.HTML, "Formatted text")
|
htmlDocument = null
|
||||||
),
|
),
|
||||||
MessagesTimelineItemUnknownContent,
|
MessagesTimelineItemUnknownContent,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue