Commit e91b520d authored by Leonardo Aramaki's avatar Leonardo Aramaki

Fix emoji parsing on messages

parent 29fc25ea
...@@ -11,7 +11,6 @@ import android.provider.Browser ...@@ -11,7 +11,6 @@ import android.provider.Browser
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v4.content.res.ResourcesCompat import android.support.v4.content.res.ResourcesCompat
import android.text.Layout import android.text.Layout
import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.style.* import android.text.style.*
import android.util.Patterns import android.util.Patterns
...@@ -25,6 +24,7 @@ import chat.rocket.common.model.SimpleUser ...@@ -25,6 +24,7 @@ import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import org.commonmark.node.AbstractVisitor import org.commonmark.node.AbstractVisitor
import org.commonmark.node.BlockQuote import org.commonmark.node.BlockQuote
import org.commonmark.node.Document
import org.commonmark.node.Text import org.commonmark.node.Text
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder import ru.noties.markwon.SpannableBuilder
...@@ -36,13 +36,6 @@ import javax.inject.Inject ...@@ -36,13 +36,6 @@ import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) { class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
private val parser = Markwon.createParser() private val parser = Markwon.createParser()
private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.white, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
/** /**
* Render a markdown text message to Spannable. * Render a markdown text message to Spannable.
...@@ -64,49 +57,16 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -64,49 +57,16 @@ class MessageParser @Inject constructor(val context: Application, private val co
parentNode.appendChild(quoteNode) parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length)) quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}") quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(EmojiVisitor(builder)) quoteNode.accept(EmojiVisitor(builder, configuration))
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
} }
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(builder)) if (message.mentions != null) {
val result = builder.text() parentNode.accept(MentionVisitor(context, builder, message.mentions!!, selfUsername))
applySpans(result, message, selfUsername)
return result
} }
parentNode.accept(EmojiVisitor(builder, configuration))
private fun applySpans(text: CharSequence, message: Message, currentUser: String?) { return builder.text()
if (text !is Spannable || !containsAnyMentions(text, message.mentions)) return
applyMentionSpans(text, message.mentions!!, currentUser)
}
private fun containsAnyMentions(text: CharSequence, mentions: List<SimpleUser>?): Boolean {
return mentions != null && mentions.isNotEmpty() ||
text.contains("@all", true) ||
text.contains("@here", true)
}
private fun applyMentionSpans(text: Spannable, mentions: List<SimpleUser>, currentUser: String?) {
val mentionsList = mentions.map { it.username }.toMutableList()
mentionsList.add("all")
mentionsList.add("here")
mentionsList.toList().forEach {
if (it != null) {
val mentionMe = it == currentUser || it == "all" || it == "here"
var offset = text.indexOf("@$it", 0, true)
while (offset > -1) {
val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding,
mentionMe)
// Add 1 to end offset to include the @.
val end = offset + it.length + 1
text.setSpan(usernameSpan, offset, end, 0)
offset = text.indexOf("@$it", end, true)
}
}
}
} }
// Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs. // Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
...@@ -142,16 +102,52 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -142,16 +102,52 @@ class MessageParser @Inject constructor(val context: Application, private val co
} }
} }
class EmojiVisitor(private val builder: SpannableBuilder) : AbstractVisitor() { class MentionVisitor(context: Context,
override fun visit(text: Text) { private val builder: SpannableBuilder,
val spannable = EmojiParser.parse(text.literal) private val mentions: List<SimpleUser>,
private val currentUser: String?) : AbstractVisitor() {
private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.white, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
override fun visit(t: Text) {
val text = t.literal
val mentionsList = mentions.map { it.username }.toMutableList()
mentionsList.add("all")
mentionsList.add("here")
mentionsList.toList().forEach {
if (it != null) {
val mentionMe = it == currentUser || it == "all" || it == "here"
var offset = text.indexOf("@$it", 0, true)
while (offset > -1) {
val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding,
mentionMe)
// Add 1 to end offset to include the @.
val end = offset + it.length + 1
builder.setSpan(usernameSpan, offset, end, 0)
offset = text.indexOf("@$it", end, true)
}
}
}
}
}
class EmojiVisitor(private val builder: SpannableBuilder, configuration: SpannableConfiguration)
: SpannableMarkdownVisitor(configuration, builder) {
override fun visit(document: Document) {
val spannable = EmojiParser.parse(builder.text())
if (spannable is Spanned) { if (spannable is Spanned) {
val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java) val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java)
spans.forEach { spans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0) builder.setSpan(it, spannable.getSpanStart(it),
spannable.getSpanEnd(it), 0)
} }
} }
visitChildren(text)
} }
} }
...@@ -272,13 +268,11 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -272,13 +268,11 @@ class MessageParser @Inject constructor(val context: Application, private val co
bottom: Int, bottom: Int,
paint: Paint) { paint: Paint) {
val length = paint.measureText(text.subSequence(start, end).toString()) val length = paint.measureText(text.subSequence(start, end).toString())
val rect = RectF(x, top.toFloat(), x + length + padding * 2, val rect = RectF(x, top.toFloat(), x + length + padding * 2, bottom.toFloat())
bottom.toFloat())
paint.color = backgroundColor paint.color = backgroundColor
canvas.drawRoundRect(rect, radius, radius, paint) canvas.drawRoundRect(rect, radius, radius, paint)
paint.color = textColor paint.color = textColor
canvas.drawText(text, start, end, x + padding, y.toFloat(), paint) canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
} }
} }
} }
\ No newline at end of file
...@@ -14,15 +14,21 @@ class EmojiParser { ...@@ -14,15 +14,21 @@ class EmojiParser {
*/ */
fun parse(text: CharSequence): CharSequence { fun parse(text: CharSequence): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true) val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannableString = SpannableString.valueOf(unicodedText) var spannable = SpannableString.valueOf(unicodedText)
// Look for groups of emojis, set a CustomTypefaceSpan with the emojione font val typeface = EmojiRepository.cachedTypeface
val length = spannableString.length // Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length
var inEmoji = false var inEmoji = false
var emojiStart = 0 var emojiStart = 0
var offset = 0 var offset = 0
while (offset < length) { while (offset < length) {
val codepoint = unicodedText.codePointAt(offset) val codepoint = unicodedText.codePointAt(offset)
val count = Character.charCount(codepoint) val count = Character.charCount(codepoint)
// Skip control characters.
if (codepoint == 0x2028) {
offset += count
continue
}
if (codepoint >= 0x200) { if (codepoint >= 0x200) {
if (!inEmoji) { if (!inEmoji) {
emojiStart = offset emojiStart = offset
...@@ -30,18 +36,25 @@ class EmojiParser { ...@@ -30,18 +36,25 @@ class EmojiParser {
inEmoji = true inEmoji = true
} else { } else {
if (inEmoji) { if (inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface), spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
inEmoji = false inEmoji = false
} }
offset += count offset += count
if (offset >= length && inEmoji) { if (offset >= length && inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface), spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
return spannableString return spannable
}
private fun calculateSurrogatePairs(scalar: Int): Pair<Int, Int> {
val temp: Int = (scalar - 0x10000) / 0x400
val s1: Int = Math.floor(temp.toDouble()).toInt() + 0xD800
val s2: Int = ((scalar - 0x10000) % 0x400) + 0xDC00
return Pair(s1, s2)
} }
} }
} }
\ No newline at end of file
...@@ -54,6 +54,10 @@ object EmojiRepository { ...@@ -54,6 +54,10 @@ object EmojiRepository {
*/ */
fun getAll() = ALL_EMOJIS fun getAll() = ALL_EMOJIS
// fun findEmojiByUnicode(unicode: Int) {
// ALL_EMOJIS.find { it.unicode == }
// }
/** /**
* Get all emojis for a given category. * Get all emojis for a given category.
* *
...@@ -119,10 +123,7 @@ object EmojiRepository { ...@@ -119,10 +123,7 @@ object EmojiRepository {
var result: String = input.toString() var result: String = input.toString()
while (matcher.find()) { while (matcher.find()) {
val unicode = shortNameToUnicode.get(":${matcher.group(1)}:") val unicode = shortNameToUnicode.get(":${matcher.group(1)}:") ?: continue
if (unicode == null) {
continue
}
if (supported) { if (supported) {
result = result.replace(":" + matcher.group(1) + ":", unicode) result = result.replace(":" + matcher.group(1) + ":", unicode)
...@@ -159,9 +160,7 @@ object EmojiRepository { ...@@ -159,9 +160,7 @@ object EmojiRepository {
private fun buildStringListFromJsonArray(array: JSONArray): List<String> { private fun buildStringListFromJsonArray(array: JSONArray): List<String> {
val list = ArrayList<String>(array.length()) val list = ArrayList<String>(array.length())
for (i in 0..array.length() - 1) { (0 until array.length()).mapTo(list) { array.getString(it) }
list.add(array.getString(i))
}
return list return list
} }
......
...@@ -17,22 +17,22 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac ...@@ -17,22 +17,22 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) { private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int val oldStyle: Int
val old = paint.getTypeface() val old = paint.typeface
if (old == null) { if (old == null) {
oldStyle = 0 oldStyle = 0
} else { } else {
oldStyle = old.getStyle() oldStyle = old.style
} }
val fake = oldStyle and tf.style.inv() val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) { if (fake and Typeface.BOLD != 0) {
paint.setFakeBoldText(true) paint.isFakeBoldText = true
} }
if (fake and Typeface.ITALIC != 0) { if (fake and Typeface.ITALIC != 0) {
paint.setTextSkewX(-0.25f) paint.textSkewX = -0.25f
} }
paint.setTypeface(tf) paint.typeface = tf
} }
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment