Unverified Commit fbd3e3f5 authored by Kiryl Vashyla's avatar Kiryl Vashyla Committed by GitHub

Merge branch 'develop' into fix/chat_history_sync

parents 4f56377f 9e631abd
......@@ -134,6 +134,8 @@ dependencies {
implementation libraries.frescoWebP
implementation libraries.frescoAnimatedWebP
implementation libraries.glide
kapt libraries.kotshiCompiler
implementation libraries.kotshiApi
......
......@@ -18,7 +18,6 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.SITE_URL
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.util.setupFabric
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco
......@@ -84,7 +83,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
context = WeakReference(applicationContext)
AndroidThreeTen.init(this)
EmojiRepository.load(this)
setupFabric(this)
setupFresco()
......
package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.method.LinkMovementMethod
import android.text.style.ImageSpan
import android.view.View
import androidx.core.view.isVisible
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 +19,7 @@ class MessageViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener) {
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
init {
with(itemView) {
......@@ -26,22 +30,26 @@ class MessageViewHolder(
override fun bindViews(data: MessageUiModel) {
with(itemView) {
day_marker_layout.visibility = if (data.showDayMarker) {
day.text = data.currentDayMarkerText
View.VISIBLE
} else {
View.GONE
}
day.text = data.currentDayMarkerText
day_marker_layout.isVisible = data.showDayMarker
if (data.isFirstUnread) {
new_messages_notif.visibility = View.VISIBLE
} else {
new_messages_notif.visibility = View.GONE
}
new_messages_notif.isVisible = data.isFirstUnread
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)
......@@ -64,4 +72,22 @@ class MessageViewHolder(
}
}
}
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)
}
}
}
......@@ -148,9 +148,4 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
/**
* Open a DM with the user in the given [chatRoom] and pass the [permalink] for the message
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
}
......@@ -6,9 +6,13 @@ 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
......@@ -51,12 +55,14 @@ import chat.rocket.android.emoji.EmojiKeyboardPopup
import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiPickerPopup
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.emoji.internal.isCustom
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.server.domain.AnalyticsTrackingInteractor
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
......@@ -72,6 +78,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
......@@ -80,6 +87,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
......@@ -132,7 +141,8 @@ internal const val MENU_ACTION_FAVORITE_MESSAGES = 5
internal const val MENU_ACTION_FILES = 6
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener,
ChatRoomAdapter.OnActionSelected {
ChatRoomAdapter.OnActionSelected, Drawable.Callback {
@Inject
lateinit var presenter: ChatRoomPresenter
@Inject
......@@ -396,10 +406,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) {
}
private val layoutChangeListener =
View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
val y = oldBottom - bottom
......@@ -642,8 +648,12 @@ 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.setSelection(cursorPosition + emoji.unicode.length)
context?.let {
val offset = if (!emoji.isCustom()) emoji.unicode.length else emoji.shortname.length
val parsed = if (emoji.isCustom()) emoji.shortname else EmojiParser.parse(it, emoji.shortname)
text_message.text?.insert(cursorPosition, parsed)
text_message.setSelection(cursorPosition + offset)
}
}
}
......@@ -774,9 +784,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) {
......@@ -788,6 +800,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
},
true
)
subscribeComposeTextMessage()
emojiKeyboardPopup =
EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
......@@ -976,6 +989,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
(activity as ChatRoomActivity).showToolbarTitle(toolbarTitle)
}
override fun unscheduleDrawable(who: Drawable?, what: Runnable?) {
text_message?.removeCallbacks(what)
}
override fun invalidateDrawable(who: Drawable?) {
text_message?.invalidate()
}
override fun scheduleDrawable(who: Drawable?, what: Runnable?, `when`: Long) {
text_message?.postDelayed(what, `when`)
}
override fun showMessageInfo(id: String) {
presenter.messageInfo(id)
}
......
......@@ -510,7 +510,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)
)
......
......@@ -7,6 +7,7 @@ import android.graphics.Paint
import android.graphics.RectF
import android.text.Spanned
import android.text.style.ClickableSpan
import android.text.style.ImageSpan
import android.text.style.ReplacementSpan
import android.util.Patterns
import android.view.View
......@@ -25,7 +26,6 @@ import org.commonmark.node.Document
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.Text
import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder
......@@ -60,11 +60,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))
}
......@@ -126,16 +126,29 @@ class MessageParser @Inject constructor(
}
class EmojiVisitor(
private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder
) : SpannableMarkdownVisitor(configuration, builder) {
private val emojiSize = context.resources.getDimensionPixelSize(R.dimen.radius_mention)
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)
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)
}
emojiImageSpans.forEach {
it.drawable?.setBounds(0, 0, emojiSize, emojiSize)
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
......@@ -230,4 +243,4 @@ class MessageParser @Inject constructor(
canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
}
}
}
\ No newline at end of file
}
package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.uimodel.NavHeaderUiModel
import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper
......@@ -30,6 +35,7 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.model.UserStatus
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getCustomEmojis
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken
......@@ -125,6 +131,38 @@ class MainPresenter @Inject constructor(
}
}
/**
* Load all emojis for the current server. Simple emojis are always the same for every server,
* but custom emojis vary according to the its url.
*/
fun loadEmojis() {
launchUI(strategy) {
EmojiRepository.setCurrentServerUrl(currentServer)
val customEmojiList = mutableListOf<Emoji>()
try {
for (customEmoji in retryIO("getCustomEmojis()") { client.getCustomEmojis() }) {
customEmojiList.add(Emoji(
shortname = ":${customEmoji.name}:",
category = EmojiCategory.CUSTOM.name,
url = "$currentServer/emoji-custom/${customEmoji.name}.${customEmoji.extension}",
count = 0,
fitzpatrick = Fitzpatrick.Default.type,
keywords = customEmoji.aliases,
shortnameAlternates = customEmoji.aliases,
siblings = mutableListOf(),
unicode = "",
isDefault = true
))
}
EmojiRepository.load(view as Context, customEmojis = customEmojiList)
} catch (ex: RocketChatException) {
Timber.e(ex)
EmojiRepository.load(view as Context)
}
}
}
/**
* Logout from current server.
*/
......
......@@ -76,6 +76,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
presenter.connect()
presenter.loadServerAccounts()
presenter.loadCurrentInfo()
presenter.loadEmojis()
setupToolbar()
setupNavigationView()
}
......
......@@ -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)
......
......@@ -31,6 +31,7 @@
<dimen name="supposed_keyboard_height">252dp</dimen>
<dimen name="picker_popup_height">250dp</dimen>
<dimen name="picker_popup_width">300dp</dimen>
<dimen name="emoji_size">22dp</dimen>
<!--Toolbar-->
<dimen name="toolbar_height">56dp</dimen>
......
......@@ -5,7 +5,7 @@ ext {
compileSdk : 28,
targetSdk : 28,
minSdk : 21,
buildTools : '28.0.1',
buildTools : '28.0.2',
dokka : '0.9.16',
// For app
......@@ -47,6 +47,7 @@ ext {
frescoImageViewer : '0.5.1',
markwon : '1.1.0',
aVLoadingIndicatorView: '2.1.3',
glide : '4.8.0-SNAPSHOT',
// For wearable
wear : '2.3.0',
......@@ -106,6 +107,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}",
......
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion versions.compileSdk
......@@ -14,6 +15,11 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
......@@ -34,6 +40,10 @@ dependencies {
implementation libraries.constraintlayout
implementation libraries.recyclerview
implementation libraries.material
implementation libraries.glide
kapt libraries.glideProcessor
implementation libraries.room
kapt libraries.roomProcessor
}
kotlin {
......
......@@ -19,3 +19,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# for DexGuard only
-keepresourcexmlelements manifest/application/meta-data@value=GlideModule
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
}
package chat.rocket.android.emoji
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity
data class Emoji(
val shortname: String,
val shortnameAlternates: List<String>,
val unicode: String,
val keywords: List<String>,
val category: String,
val count: Int = 0,
val siblings: MutableCollection<Emoji> = mutableListOf(),
val fitzpatrick: Fitzpatrick = Fitzpatrick.Default
)
\ No newline at end of file
@PrimaryKey
var shortname: String = "",
var shortnameAlternates: List<String> = listOf(),
var unicode: String = "",
@Ignore val keywords: List<String> = listOf(),
var category: String = "",
var count: Int = 0,
var siblings: MutableList<String> = mutableListOf(), // Siblings are the same emoji with different skin tones.
var fitzpatrick: String = Fitzpatrick.Default.type,
var url: String? = null, // Filled for custom emojis
var isDefault: Boolean = true // Tell if this is the default emoji if it has siblings (usually a yellow-toned one).
)
package chat.rocket.android.emoji
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy.IGNORE
import androidx.room.Query
import androidx.room.Update
@Dao
interface EmojiDao {
@Query("SELECT * FROM emoji")
fun loadAllEmojis(): List<Emoji>
@Query("SELECT * FROM emoji WHERE url IS NULL")
fun loadSimpleEmojis(): List<Emoji>
@Query("SELECT * FROM emoji WHERE url IS NOT NULL")
fun loadAllCustomEmojis(): List<Emoji>
@Query("SELECT * FROM emoji WHERE shortname=:shortname")
fun loadEmojiByShortname(shortname: String): List<Emoji>
@Query("SELECT * FROM emoji WHERE UPPER(category)=UPPER(:category)")
fun loadEmojisByCategory(category: String): List<Emoji>
@Query("SELECT * FROM emoji WHERE UPPER(category)=UPPER(:category) AND url LIKE :url")
fun loadEmojisByCategoryAndUrl(category: String, url: String): List<Emoji>
@Insert(onConflict = IGNORE)
fun insertEmoji(emoji: Emoji)
@Insert(onConflict = IGNORE)
fun insertAllEmojis(vararg emojis: Emoji)
@Update
fun updateEmoji(emoji: Emoji)
@Delete
fun deleteEmoji(emoji: Emoji)
@Query("DELETE FROM emoji")
fun deleteAll()
}
......@@ -22,6 +22,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow(context, view) {
......@@ -49,8 +51,10 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
}
override fun onViewCreated(view: View) {
setupViewPager()
setupBottomBar()
launch(UI) {
setupViewPager()
setupBottomBar()
}
}
private fun setupBottomBar() {
......@@ -81,42 +85,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)
......@@ -148,7 +152,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
}
}
private fun setupViewPager() {
private suspend fun setupViewPager() {
context.let {
val callback = when (it) {
is EmojiKeyboardListener -> it
......@@ -167,6 +171,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
callback.onEmojiAdded(emoji)
}
})
viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter
......@@ -183,6 +188,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} else {
EmojiCategory.RECENTS.ordinal
}
viewPager.currentItem = currentTab
}
}
......
package chat.rocket.android.emoji
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ImageSpan
import android.util.Log
import chat.rocket.android.emoji.internal.GlideApp
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.gif.GifDrawable
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.async
class EmojiParser {
companion object {
private val regex = ":[\\w]+:".toRegex()
/**
* Parses a text string containing unicode characters and/or shortnames to a rendered
* Spannable.
......@@ -15,10 +29,18 @@ 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)
val typeface = EmojiRepository.cachedTypeface
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 = try {
EmojiRepository.cachedTypeface
} catch (ex: UninitializedPropertyAccessException) {
// swallow this exception and create typeface now
Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
}
// Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length
var inEmoji = false
......@@ -32,6 +54,7 @@ class EmojiParser {
offset += count
continue
}
if (codepoint >= 0x200) {
if (!inEmoji) {
emojiStart = offset
......@@ -40,17 +63,64 @@ 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)
}
}
return spannable
val customEmojis = EmojiRepository.getCustomEmojis()
val px = context.resources.getDimensionPixelSize(R.dimen.custom_emoji_small)
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)) {
GlideApp.with(context).asGif()
} else {
GlideApp.with(context).asBitmap()
}
val futureTarget = glideRequest
.diskCacheStrategy(DiskCacheStrategy.ALL)
.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, image.intrinsicWidth, image.intrinsicHeight)
spannable.setSpan(ImageSpan(image), range.start,
range.endInclusive + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
} catch (ex: Throwable) {
Log.e("EmojiParser", "", ex)
}
}
}
}
}
}
fun parseAsync(
context: Context,
text: CharSequence,
factory: Spannable.Factory? = null
): Deferred<CharSequence> {
return async(CommonPool) { parse(context, text, factory) }
}
}
}
\ No newline at end of file
}
......@@ -14,6 +14,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import kotlinx.android.synthetic.main.emoji_picker.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
class EmojiPickerPopup(context: Context) : Dialog(context) {
......@@ -27,8 +29,10 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
setContentView(R.layout.emoji_picker)
tabs.setupWithViewPager(pager_categories)
setupViewPager()
setSize()
launch(UI) {
setupViewPager()
setSize()
}
}
private fun setSize() {
......@@ -39,7 +43,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
window.setLayout(dialogWidth, dialogHeight)
}
private fun setupViewPager() {
private suspend fun setupViewPager() {
adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
......
......@@ -7,12 +7,17 @@ import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.EmojiTypefaceSpan
import chat.rocket.android.emoji.R
internal enum class EmojiCategory {
enum class EmojiCategory {
RECENTS {
override fun resourceIcon() = R.drawable.ic_emoji_recents
override fun textIcon() = getTextIconFor("\uD83D\uDD58")
},
CUSTOM {
override fun resourceIcon() = R.drawable.ic_emoji_custom
override fun textIcon() = getTextIconFor("\uD83D\uDD58")
},
PEOPLE() {
override fun resourceIcon() = R.drawable.ic_emoji_people
......@@ -65,4 +70,4 @@ internal enum class EmojiCategory {
setSpan(span, 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
}
}
\ No newline at end of file
}
package chat.rocket.android.emoji.internal
import android.content.Context
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.engine.cache.ExternalPreferredCacheDiskCacheFactory
import com.bumptech.glide.module.AppGlideModule
@GlideModule
class EmojiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(ExternalPreferredCacheDiskCacheFactory(context))
}
}
......@@ -14,7 +14,9 @@ import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.R
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.emoji_category_layout.view.*
import kotlinx.android.synthetic.main.emoji_image_row_item.view.*
import kotlinx.android.synthetic.main.emoji_row_item.view.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
......@@ -43,22 +45,33 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
container.addView(view)
launch(UI) {
val currentServerUrl = EmojiRepository.getCurrentServerUrl()
val emojis = if (category != EmojiCategory.RECENTS) {
EmojiRepository.getEmojiSequenceByCategory(category)
if (category == EmojiCategory.CUSTOM) {
currentServerUrl?.let { url ->
EmojiRepository.getEmojiSequenceByCategoryAndUrl(category, url)
} ?: emptySequence()
} else {
EmojiRepository.getEmojiSequenceByCategory(category)
}
} else {
sequenceOf(*EmojiRepository.getRecents().toTypedArray())
}
val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
if (adapters[category] == null) {
val adapter = EmojiAdapter(listener = listener)
emoji_recycler_view.adapter = adapter
adapters[category] = adapter
adapter.addEmojisFromSequence(emojis)
}
adapters[category]!!.setFitzpatrick(fitzpatrick)
}
}
return view
}
......@@ -88,20 +101,24 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
private val listener: EmojiKeyboardListener
) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private val CUSTOM = 1
private val NORMAL = 2
private val allEmojis = mutableListOf<Emoji>()
private val emojis = mutableListOf<Emoji>()
fun addEmojis(emojis: List<Emoji>) {
this.emojis.clear()
this.emojis.addAll(emojis)
notifyDataSetChanged()
override fun getItemViewType(position: Int): Int {
return if (emojis[position].isCustom()) CUSTOM else NORMAL
}
suspend fun addEmojisFromSequence(emojiSequence: Sequence<Emoji>) {
withContext(CommonPool) {
emojiSequence.forEachIndexed { index, emoji ->
withContext(UI) {
emojis.add(emoji)
notifyItemInserted(index)
allEmojis.add(emoji)
if (emoji.isDefault) {
emojis.add(emoji)
notifyItemInserted(emojis.size - 1)
}
}
}
}
......@@ -115,12 +132,26 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
val emoji = emojis[position]
holder.bind(
emoji.siblings.find { it.fitzpatrick == fitzpatrick } ?: emoji
if (fitzpatrick != Fitzpatrick.Default) {
emoji.siblings.find {
it.endsWith("${fitzpatrick.type}:")
}?.let { shortname ->
allEmojis.firstOrNull {
it.shortname == shortname
}
} ?: emoji
} else {
emoji
}
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false)
val view = if (viewType == CUSTOM) {
LayoutInflater.from(parent.context).inflate(R.layout.emoji_image_row_item, parent, false)
} else {
LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false)
}
return EmojiRowViewHolder(view, listener)
}
......@@ -134,16 +165,26 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
fun bind(emoji: Emoji) {
with(itemView) {
val parsedUnicode = unicodeCache[emoji.unicode]
emoji_view.setSpannableFactory(spannableFactory)
emoji_view.text = if (parsedUnicode == null) {
EmojiParser.parse(emoji.unicode, spannableFactory).let {
unicodeCache[emoji.unicode] = it
it
if (emoji.unicode.isNotEmpty()) {
// Handle simple emoji.
val parsedUnicode = unicodeCache[emoji.unicode]
emoji_view.setSpannableFactory(spannableFactory)
emoji_view.text = if (parsedUnicode == null) {
EmojiParser.parse(itemView.context, emoji.unicode, spannableFactory).let {
unicodeCache[emoji.unicode] = it
it
}
} else {
parsedUnicode
}
} else {
parsedUnicode
// Handle custom emoji.
GlideApp.with(context)
.load(emoji.url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(emoji_image_view)
}
itemView.setOnClickListener {
listener.onEmojiAdded(emoji)
}
......@@ -155,4 +196,4 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
private val unicodeCache = mutableMapOf<CharSequence, CharSequence>()
}
}
}
\ No newline at end of file
}
package chat.rocket.android.emoji.internal
import chat.rocket.android.emoji.Emoji
fun Emoji.isCustom(): Boolean = this.url != null
package chat.rocket.android.emoji.internal.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiDao
@Database(entities = [Emoji::class], version = 1)
@TypeConverters(StringListConverter::class)
abstract class EmojiDatabase : RoomDatabase() {
abstract fun emojiDao(): EmojiDao
companion object : SingletonHolder<EmojiDatabase, Context>({
Room.databaseBuilder(it.applicationContext,
EmojiDatabase::class.java, "emoji.db")
.fallbackToDestructiveMigration()
.build()
})
}
open class SingletonHolder<out T, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@kotlin.jvm.Volatile
private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
package chat.rocket.android.emoji.internal.db
import androidx.room.TypeConverter
class StringListConverter {
@TypeConverter
fun fromStringList(list: List<String>?): String {
return list?.joinToString(separator = ",") ?: ""
}
@TypeConverter
fun fromString(value: String?): List<String> {
return value?.split(",") ?: emptyList()
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center">
<ImageView
android:id="@+id/emoji_image_view"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
tools:src="@tools:sample/avatars" />
</FrameLayout>
......@@ -6,6 +6,7 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:textColor="#000000"
android:textSize="26sp"
tools:text="😀" />
......@@ -5,5 +5,6 @@
<dimen name="supposed_keyboard_height">252dp</dimen>
<dimen name="picker_popup_height">250dp</dimen>
<dimen name="picker_popup_width">300dp</dimen>
</resources>
\ No newline at end of file
<dimen name="custom_emoji_large">32dp</dimen>
<dimen name="custom_emoji_small">22dp</dimen>
</resources>
#Wed Aug 01 22:00:00 EDT 2018
#Mon Aug 06 11:30:07 BRT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
......
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