Add plain text representation of messages (#1850)
* Add plain text representation of messages. This is used in the room list as the last message in a room, in the message summary when a message is selected, in the 'replying to' block, in the 'replied to' block in a message in the timeline, and in notifications.
This commit is contained in:
parent
e13b204f4b
commit
d413aa1ee3
24 changed files with 544 additions and 39 deletions
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.messages
|
||||
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
/**
|
||||
* Converts the HTML string [FormattedBody.body] to a [Document] by parsing it.
|
||||
* If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`.
|
||||
*
|
||||
* This will also make sure mentions are prefixed with `@`.
|
||||
*
|
||||
* @param prefix if not null, the prefix will be inserted at the beginning of the message.
|
||||
*/
|
||||
fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? {
|
||||
return takeIf { it.format == MessageFormat.HTML }?.body?.let { formattedBody ->
|
||||
val dom = if (prefix != null) {
|
||||
Jsoup.parse("$prefix $formattedBody")
|
||||
} else {
|
||||
Jsoup.parse(formattedBody)
|
||||
}
|
||||
|
||||
// Prepend `@` to mentions
|
||||
fixMentions(dom)
|
||||
|
||||
dom
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixMentions(dom: Document) {
|
||||
val links = dom.getElementsByTag("a")
|
||||
links.forEach {
|
||||
if (it.hasAttr("href")) {
|
||||
val link = PermalinkParser.parse(it.attr("href"))
|
||||
if (link is PermalinkData.UserLink && !it.text().startsWith("@")) {
|
||||
it.prependText("@")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.messages
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.Node
|
||||
import org.jsoup.nodes.TextNode
|
||||
import org.jsoup.select.NodeVisitor
|
||||
|
||||
/**
|
||||
* Converts the HTML string in [TextMessageType.formatted] to a plain text representation by parsing it and removing all formatting.
|
||||
* If the message is not formatted or the format is not [MessageFormat.HTML], the [TextMessageType.body] is returned instead.
|
||||
*/
|
||||
fun TextMessageType.toPlainText() = formatted?.toPlainText() ?: body
|
||||
|
||||
/**
|
||||
* Converts the HTML string in [FormattedBody.body] to a plain text representation by parsing it and removing all formatting.
|
||||
* If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`.
|
||||
* @param prefix if not null, the prefix will be inserted at the beginning of the message.
|
||||
*/
|
||||
fun FormattedBody.toPlainText(prefix: String? = null): String? {
|
||||
return this.toHtmlDocument(prefix)?.toPlainText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the HTML [Document] to a plain text representation by parsing it and removing all formatting.
|
||||
*/
|
||||
fun Document.toPlainText(): String {
|
||||
val visitor = PlainTextNodeVisitor()
|
||||
traverse(visitor)
|
||||
return visitor.build()
|
||||
}
|
||||
|
||||
private class PlainTextNodeVisitor : NodeVisitor {
|
||||
private val builder = StringBuilder()
|
||||
|
||||
override fun head(node: Node, depth: Int) {
|
||||
if (node is TextNode && node.text().isNotBlank()) {
|
||||
builder.append(node.text())
|
||||
} else if (node is Element && node.tagName() == "li") {
|
||||
val index = node.elementSiblingIndex()
|
||||
val isOrdered = node.parent()?.nodeName()?.lowercase() == "ol"
|
||||
if (isOrdered) {
|
||||
builder.append("${index + 1}. ")
|
||||
} else {
|
||||
builder.append("• ")
|
||||
}
|
||||
} else if (node is Element && node.isBlock && builder.lastOrNull() != '\n') {
|
||||
builder.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
override fun tail(node: Node, depth: Int) {
|
||||
fun nodeIsBlockButNotLastOne(node: Node) = node is Element && node.isBlock && node.lastElementSibling() !== node
|
||||
fun nodeIsLineBreak(node: Node) = node.nodeName().lowercase() == "br"
|
||||
if (nodeIsBlockButNotLastOne(node) || nodeIsLineBreak(node)) {
|
||||
builder.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
return builder.toString().trim()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue