Commit e2c4ebf6 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Show custom emojis on messages (including animated gifs)

parent cfcc4f62
......@@ -115,6 +115,8 @@ dependencies {
implementation libraries.frescoWebP
implementation libraries.frescoAnimatedWebP
implementation libraries.glide
kapt libraries.kotshiCompiler
implementation libraries.kotshiApi
......
package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ImageSpan
import android.view.View
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.isSystemMessage
import com.bumptech.glide.load.resource.gif.GifDrawable
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
......@@ -15,7 +21,24 @@ class MessageViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener) {
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
override fun unscheduleDrawable(who: Drawable?, what: Runnable?) {
with(itemView) {
text_content.removeCallbacks(what)
}
}
override fun invalidateDrawable(p0: Drawable?) {
with(itemView) {
text_content.invalidate()
}
}
override fun scheduleDrawable(who: Drawable?, what: Runnable?, w: Long) {
with(itemView) {
text_content.postDelayed(what, w)
}
}
init {
with(itemView) {
......@@ -31,15 +54,30 @@ class MessageViewHolder(
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
if (data.content is Spannable) {
val spans = data.content.getSpans(0, data.content.length, ImageSpan::class.java)
spans.forEach {
if (it.drawable is GifDrawable) {
it.drawable.callback = this@MessageViewHolder
(it.drawable as GifDrawable).start()
}
}
}
text_content.text_content.text = data.content
image_avatar.setImageURI(data.avatar)
text_content.setTextColor(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
data.message.let {
text_edit_indicator.isVisible = !it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
}
if (data.unread == null) {
read_receipt_view.isVisible = false
} else {
......
......@@ -618,7 +618,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun onEmojiAdded(emoji: Emoji) {
val cursorPosition = text_message.selectionStart
if (cursorPosition > -1) {
text_message.text?.insert(cursorPosition, EmojiParser.parse(emoji.shortname))
text_message.text?.insert(cursorPosition, EmojiParser.parse(context!!, emoji.shortname))
text_message.setSelection(cursorPosition + emoji.unicode.length)
}
}
......
......@@ -457,7 +457,7 @@ class UiModelMapper @Inject constructor(
list.add(
ReactionUiModel(messageId = message.id,
shortname = shortname,
unicode = EmojiParser.parse(shortname),
unicode = EmojiParser.parse(context, shortname),
count = count,
usernames = usernames)
)
......
......@@ -9,6 +9,7 @@ import android.net.Uri
import androidx.core.content.res.ResourcesCompat
import android.text.Spanned
import android.text.style.ClickableSpan
import android.text.style.ImageSpan
import android.text.style.ReplacementSpan
import android.text.style.StyleSpan
import android.util.Patterns
......@@ -62,11 +63,11 @@ class MessageParser @Inject constructor(
}
}
val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text, true)
val content = EmojiRepository.shortnameToUnicode(text)
val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(MarkdownVisitor(configuration, builder))
parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(configuration, builder))
parentNode.accept(EmojiVisitor(context, configuration, builder))
message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername))
}
......@@ -128,17 +129,22 @@ class MessageParser @Inject constructor(
}
class EmojiVisitor(
private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder
) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(document: Document) {
val spannable = EmojiParser.parse(builder.text())
val spannable = EmojiParser.parse(context, builder.text())
if (spannable is Spanned) {
val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java)
val spans2 = spannable.getSpans(0, spannable.length, ImageSpan::class.java)
spans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0)
}
spans2.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0)
}
}
}
}
......@@ -240,4 +246,4 @@ class MessageParser @Inject constructor(
canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
}
}
}
\ No newline at end of file
}
......@@ -66,12 +66,13 @@ var TextView.content: CharSequence?
Markwon.unscheduleDrawables(this)
Markwon.unscheduleTableRows(this)
if (value is Spanned) {
val result = EmojiParser.parse(value.toString()) as Spannable
val context = this.context
val result = EmojiParser.parse(context, value.toString()) as Spannable
val end = if (value.length > result.length) result.length else value.length
TextUtils.copySpansFrom(value, 0, end, Any::class.java, result, 0)
text = result
} else {
val result = EmojiParser.parse(value.toString()) as Spannable
val result = EmojiParser.parse(context, value.toString()) as Spannable
text = result
}
Markwon.scheduleDrawables(this)
......
......@@ -46,6 +46,7 @@ ext {
frescoImageViewer : '0.5.1',
markwon : '1.0.6',
aVLoadingIndicatorView: '2.1.3',
glide : '4.7.1',
// For wearable
wear : '2.3.0',
......@@ -107,6 +108,8 @@ ext {
kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}",
frescoImageViewer : "com.github.luciofm:FrescoImageViewer:${versions.frescoImageViewer}",
glide : "com.github.bumptech.glide:glide:${versions.glide}",
glideProcessor : "com.github.bumptech.glide:compiler:${versions.glide}",
markwon : "ru.noties:markwon:${versions.markwon}",
......
......@@ -34,8 +34,8 @@ dependencies {
implementation libraries.constraintlayout
implementation libraries.recyclerview
implementation libraries.material
implementation 'com.github.bumptech.glide:glide:4.7.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1'
implementation libraries.glide
annotationProcessor libraries.glideProcessor
}
kotlin {
......
......@@ -81,42 +81,42 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
.create()
view.findViewById<TextView>(R.id.default_tone_text).also {
it.text = EmojiParser.parse(it.text)
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.Default)
}
view.findViewById<TextView>(R.id.light_tone_text).also {
it.text = EmojiParser.parse(it.text)
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.LightTone)
}
view.findViewById<TextView>(R.id.medium_light_text).also {
it.text = EmojiParser.parse(it.text)
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumLightTone)
}
view.findViewById<TextView>(R.id.medium_tone_text).also {
it.text = EmojiParser.parse(it.text)
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumTone)
}
view.findViewById<TextView>(R.id.medium_dark_tone_text).also {
it.text = EmojiParser.parse(it.text)
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumDarkTone)
}
view.findViewById<TextView>(R.id.dark_tone_text).also {
it.text = EmojiParser.parse(it.text)
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.DarkTone)
......
package chat.rocket.android.emoji
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ImageSpan
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
class EmojiParser {
companion object {
private val regex = ":[\\w]+:".toRegex()
/**
* Parses a text string containing unicode characters and/or shortnames to a rendered
* Spannable.
......@@ -15,9 +30,11 @@ class EmojiParser {
* @param factory Optional. A [Spannable.Factory] instance to reuse when creating [Spannable].
* @return A rendered Spannable containing any supported emoji.
*/
fun parse(text: CharSequence, factory: Spannable.Factory? = null): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannable = factory?.newSpannable(unicodedText) ?: SpannableString.valueOf(unicodedText)
fun parse(context: Context, text: CharSequence, factory: Spannable.Factory? = null): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text)
val spannable = factory?.newSpannable(unicodedText)
?: SpannableString.valueOf(unicodedText)
val typeface = EmojiRepository.cachedTypeface
// Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length
......@@ -40,17 +57,53 @@ class EmojiParser {
} else {
if (inEmoji) {
spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
inEmoji = false
}
offset += count
if (offset >= length && inEmoji) {
spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
val customEmojis = EmojiRepository.getCustomEmojis()
val density = context.resources.displayMetrics.density
val px = (24 * density).toInt()
return spannable.also {
regex.findAll(spannable).iterator().forEach { match ->
customEmojis.find { it.shortname.toLowerCase() == match.value.toLowerCase() }?.let {
it.url?.let { url ->
try {
val glideRequest = if (url.endsWith("gif", true)) {
Glide.with(context).asGif()
} else {
Glide.with(context).asBitmap()
}
val futureTarget = glideRequest.load(url).submit(px, px)
val range = match.range
futureTarget.get()?.let { image ->
if (image is Bitmap) {
spannable.setSpan(ImageSpan(context, image), range.start,
range.endInclusive + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} else if (image is GifDrawable) {
image.setBounds(0, 0, px, px)
spannable.setSpan(ImageSpan(image), range.start,
range.endInclusive + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
} catch (ex: Throwable) {
Log.e("EmojiParser", "", ex)
}
}
}
}
}
return spannable
}
}
}
\ No newline at end of file
}
......@@ -21,10 +21,12 @@ object EmojiRepository {
private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>()
private var customEmojis: List<Emoji> = emptyList()
private lateinit var preferences: SharedPreferences
internal lateinit var cachedTypeface: Typeface
fun load(context: Context, customEmojis: List<Emoji> = emptyList(), path: String = "emoji.json") {
this.customEmojis = customEmojis
preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
ALL_EMOJIS.clear()
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
......@@ -133,6 +135,8 @@ object EmojiRepository {
preferences.edit().putString(PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
}
internal fun getCustomEmojis(): List<Emoji> = customEmojis
/**
* Get all recently used emojis ordered by usage count.
*
......@@ -157,7 +161,7 @@ object EmojiRepository {
/**
* Replace shortnames to unicode characters.
*/
fun shortnameToUnicode(input: CharSequence, removeIfUnsupported: Boolean): String {
fun shortnameToUnicode(input: CharSequence): String {
val matcher = SHORTNAME_PATTERN.matcher(input)
var result: String = input.toString()
......
......@@ -151,7 +151,7 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
val parsedUnicode = unicodeCache[emoji.unicode]
emoji_view.setSpannableFactory(spannableFactory)
emoji_view.text = if (parsedUnicode == null) {
EmojiParser.parse(emoji.unicode, spannableFactory).let {
EmojiParser.parse(itemView.context, emoji.unicode, spannableFactory).let {
unicodeCache[emoji.unicode] = it
it
}
......
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