Commit e286abc5 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Let emojis to be rendered on the composer

parent e2c4ebf6
......@@ -72,7 +72,7 @@ class MessageViewHolder(
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
......
......@@ -6,21 +6,24 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.os.SystemClock
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ImageSpan
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.text.bold
import androidx.core.view.isVisible
......@@ -53,10 +56,11 @@ import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiPickerPopup
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.circularRevealOrUnreveal
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
......@@ -70,6 +74,7 @@ import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import com.bumptech.glide.load.resource.gif.GifDrawable
import dagger.android.support.AndroidSupportInjection
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
......@@ -78,6 +83,8 @@ import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
......@@ -127,7 +134,9 @@ internal const val MENU_ACTION_PINNED_MESSAGES = 4
internal const val MENU_ACTION_FAVORITE_MESSAGES = 5
internal const val MENU_ACTION_FILES = 6
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener,
Drawable.Callback {
@Inject
lateinit var presenter: ChatRoomPresenter
@Inject
......@@ -428,7 +437,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} else {
if (dy < 0 && !button_fab.isVisible) {
button_fab.show()
if (newMessageCount !=0) text_count.isVisible = true
if (newMessageCount != 0) text_count.isVisible = true
}
}
}
......@@ -487,14 +496,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (isMessageReceived && button_fab.isVisible) {
newMessageCount++
if (newMessageCount <= 99)
text_count.text = newMessageCount.toString()
else
text_count.text = "99+"
if (newMessageCount <= 99)
text_count.text = newMessageCount.toString()
else
text_count.text = "99+"
text_count.isVisible = true
}
else if (!button_fab.isVisible)
} else if (!button_fab.isVisible)
recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
empty_chat_view.isVisible = adapter.itemCount == 0
......@@ -618,8 +626,29 @@ 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(context!!, emoji.shortname))
text_message.setSelection(cursorPosition + emoji.unicode.length)
context?.let { ctx ->
launch(UI) {
val parsedText = EmojiParser.parseAsync(ctx, emoji.shortname).await()
if (parsedText is Spannable) {
val spans = parsedText.getSpans(0, parsedText.length, ImageSpan::class.java)
spans.forEach {
if (it.drawable is GifDrawable) {
it.drawable.callback = this@ChatRoomFragment
(it.drawable as GifDrawable).start()
}
}
text_message.text?.insert(text_message.selectionStart, parsedText)
text_message.text?.insert(text_message.selectionStart, " ")
}
// If it has no url then it's not a custom emoji.
if (emoji.url == null) {
text_message.setSelection(cursorPosition + emoji.unicode.length)
}
}
}
}
}
......@@ -750,9 +779,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
button_send.isVisible = false
button_show_attachment_options.alpha = 1f
button_show_attachment_options.isVisible = true
activity?.supportFragmentManager?.addOnBackStackChangedListener {
println("attach")
}
activity?.supportFragmentManager?.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
......@@ -764,6 +795,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
},
true
)
subscribeComposeTextMessage()
emojiKeyboardPopup =
EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
......@@ -883,7 +915,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
clearMessageComposition(false)
if (text_message.textContent.isEmpty()) {
KeyboardHelper.showSoftKeyboard(text_message)
}
}
}
}
......@@ -951,4 +983,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setupToolbar(toolbarTitle: String) {
(activity as ChatRoomActivity).showToolbarTitle(toolbarTitle)
}
override fun unscheduleDrawable(who: Drawable?, what: Runnable?) {
text_message?.removeCallbacks(what)
}
override fun invalidateDrawable(p0: Drawable?) {
text_message?.invalidate()
}
override fun scheduleDrawable(who: Drawable?, what: Runnable?, w: Long) {
text_message?.postDelayed(what, w)
}
}
......@@ -11,7 +11,6 @@ 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
import android.view.View
import chat.rocket.android.R
......@@ -137,13 +136,18 @@ class MessageParser @Inject constructor(
override fun visit(document: Document) {
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)
val emojiOneTypefaceSpans = spannable.getSpans(0, spannable.length,
EmojiTypefaceSpan::class.java)
val emojiImageSpans = spannable.getSpans(0, spannable.length, ImageSpan::class.java)
emojiOneTypefaceSpans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
spans2.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0)
emojiImageSpans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
......
package chat.rocket.android.emoji
import android.content.Context
import androidx.appcompat.widget.AppCompatEditText
import android.text.Spanned
import android.text.style.ImageSpan
import android.util.AttributeSet
import android.view.KeyEvent
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.text.getSpans
class ComposerEditText : AppCompatEditText {
var listener: ComposerEditTextListener? = null
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
super(context, attrs, defStyleAttr) {
super(context, attrs, defStyleAttr) {
isFocusable = true
isFocusableInTouchMode = true
isClickable = true
......@@ -20,6 +24,21 @@ class ComposerEditText : AppCompatEditText {
constructor(context: Context) : this(context, null)
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
super.onSelectionChanged(selStart, selEnd)
text?.getSpans<ImageSpan>()?.forEach {
val s = text?.getSpanStart(it) ?: -1
val e = text?.getSpanEnd(it) ?: -1
val flags = if (selStart in s..e) {
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE or Spanned.SPAN_COMPOSING
} else {
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
}
text?.setSpan(it, s, e, flags)
}
}
override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
val state = keyDispatcherState
......@@ -43,4 +62,4 @@ class ComposerEditText : AppCompatEditText {
fun onKeyboardClosed()
fun onKeyboardOpened()
}
}
\ No newline at end of file
}
......@@ -2,19 +2,21 @@ package chat.rocket.android.emoji
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.graphics.drawable.BitmapDrawable
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.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.FutureTarget
import com.bumptech.glide.request.RequestFutureTarget
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.async
class EmojiParser {
......@@ -77,7 +79,6 @@ class EmojiParser {
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()
......@@ -85,7 +86,13 @@ class EmojiParser {
Glide.with(context).asBitmap()
}
val futureTarget = glideRequest.load(url).submit(px, px)
val futureTarget = glideRequest
.apply(RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.onlyRetrieveFromCache(true))
.load(url)
.submit(px, px)
val range = match.range
futureTarget.get()?.let { image ->
if (image is Bitmap) {
......@@ -105,5 +112,13 @@ class EmojiParser {
}
}
}
fun parseAsync(
context: Context,
text: CharSequence,
factory: Spannable.Factory? = null
): Deferred<CharSequence> {
return async(CommonPool) { parse(context, text, factory) }
}
}
}
......@@ -5,6 +5,9 @@ import android.content.SharedPreferences
import android.graphics.Typeface
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS
import com.bumptech.glide.Glide
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
......@@ -15,6 +18,7 @@ import java.util.regex.Pattern
import kotlin.collections.ArrayList
import kotlin.coroutines.experimental.buildSequence
object EmojiRepository {
private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE)
......@@ -26,56 +30,68 @@ object EmojiRepository {
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")
val stream = context.assets.open(path)
val emojis = loadEmojis(stream).also {
it.addAll(customEmojis)
}.toList()
for (emoji in emojis) {
val unicodeIntList = mutableListOf<Int>()
// If empty it's a custom emoji.
if (emoji.unicode.isEmpty()) {
ALL_EMOJIS.add(emoji)
continue
}
launch(CommonPool) {
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
this@EmojiRepository.customEmojis = customEmojis
preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
ALL_EMOJIS.clear()
val stream = context.assets.open(path)
val emojis = loadEmojis(stream).also {
it.addAll(customEmojis)
}.toList()
for (emoji in emojis) {
val unicodeIntList = mutableListOf<Int>()
// If empty it's a custom emoji.
if (emoji.unicode.isEmpty()) {
ALL_EMOJIS.add(emoji)
continue
}
emoji.unicode.split("-").forEach {
val value = it.toInt(16)
if (value >= 0x10000) {
val surrogatePair = calculateSurrogatePairs(value)
unicodeIntList.add(surrogatePair.first)
unicodeIntList.add(surrogatePair.second)
} else {
unicodeIntList.add(value)
emoji.unicode.split("-").forEach {
val value = it.toInt(16)
if (value >= 0x10000) {
val surrogatePair = calculateSurrogatePairs(value)
unicodeIntList.add(surrogatePair.first)
unicodeIntList.add(surrogatePair.second)
} else {
unicodeIntList.add(value)
}
}
}
val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
val emojiWithUnicode = emoji.copy(unicode = unicode)
if (hasFitzpatrick(emoji.shortname)) {
val matchResult = FITZPATRICK_REGEX.find(emoji.shortname)
val prefix = matchResult!!.groupValues[1] + ":"
val fitzpatrick = Fitzpatrick.valueOf(matchResult.groupValues[2])
val defaultEmoji = ALL_EMOJIS.firstOrNull { it.shortname == prefix }
val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick)
if (defaultEmoji != null) {
defaultEmoji.siblings.add(emojiWithFitzpatrick)
val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
val emojiWithUnicode = emoji.copy(unicode = unicode)
if (hasFitzpatrick(emoji.shortname)) {
val matchResult = FITZPATRICK_REGEX.find(emoji.shortname)
val prefix = matchResult!!.groupValues[1] + ":"
val fitzpatrick = Fitzpatrick.valueOf(matchResult.groupValues[2])
val defaultEmoji = ALL_EMOJIS.firstOrNull { it.shortname == prefix }
val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick)
if (defaultEmoji != null) {
defaultEmoji.siblings.add(emojiWithFitzpatrick)
} else {
// This emoji doesn't have a default tone, ie. :man_in_business_suit_levitating_tone1:
// In this case, the default emoji becomes the first toned one.
ALL_EMOJIS.add(emojiWithFitzpatrick)
}
} else {
// This emoji doesn't have a default tone, ie. :man_in_business_suit_levitating_tone1:
// In this case, the default emoji becomes the first toned one.
ALL_EMOJIS.add(emojiWithFitzpatrick)
ALL_EMOJIS.add(emojiWithUnicode)
}
shortNameToUnicode.apply {
put(emoji.shortname, unicode)
emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
}
} else {
ALL_EMOJIS.add(emojiWithUnicode)
}
shortNameToUnicode.apply {
put(emoji.shortname, unicode)
emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
val density = context.resources.displayMetrics.density
val px = (32 * density).toInt()
customEmojis.forEach {
val future = Glide.with(context)
.load(it.url)
.submit(px, px)
future.get()
}
}
}
......
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