Commit c173a519 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'develop' of github.com:RocketChat/Rocket.Chat.Android into new/search-messages

parents 78795815 c32b8dc6
......@@ -24,6 +24,7 @@ import androidx.annotation.DrawableRes
import androidx.core.text.bold
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
......@@ -35,6 +36,7 @@ import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.bottomsheet.MessageActionsBottomSheet
import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
......@@ -244,6 +246,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
super.onDestroyView()
}
override fun onPause() {
super.onPause()
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
emojiKeyboardPopup.dismiss()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (resultData != null && resultCode == Activity.RESULT_OK) {
when (requestCode) {
......@@ -654,22 +662,22 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun showConnectionState(state: State) {
ui {
connection_status_text.fadeIn()
text_connection_status.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
is State.Connected -> {
connection_status_text.text = getString(R.string.status_connected)
text_connection_status.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> connection_status_text.text =
is State.Disconnected -> text_connection_status.text =
getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text =
is State.Connecting -> text_connection_status.text =
getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text =
is State.Authenticating -> text_connection_status.text =
getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text =
is State.Disconnecting -> text_connection_status.text =
getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text =
is State.Waiting -> text_connection_status.text =
getString(R.string.status_waiting, state.seconds)
}
}
......@@ -685,7 +693,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
private val dismissStatus = {
connection_status_text.fadeOut()
text_connection_status.fadeOut()
}
private fun setupRecyclerView() {
......@@ -725,7 +733,20 @@ 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) {
if (f is MessageActionsBottomSheet) {
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
emojiKeyboardPopup.dismiss()
}
}
},
true
)
subscribeComposeTextMessage()
emojiKeyboardPopup =
EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
......
......@@ -290,25 +290,25 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private fun showConnectionState(state: State) {
Timber.d("Got new state: $state")
ui {
connection_status_text.fadeIn()
text_connection_status.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
is State.Connected -> {
connection_status_text.text = getString(R.string.status_connected)
text_connection_status.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> connection_status_text.text = getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = getString(R.string.status_waiting, state.seconds)
is State.Disconnected -> text_connection_status.text = getString(R.string.status_disconnected)
is State.Connecting -> text_connection_status.text = getString(R.string.status_connecting)
is State.Authenticating -> text_connection_status.text = getString(R.string.status_authenticating)
is State.Disconnecting -> text_connection_status.text = getString(R.string.status_disconnecting)
is State.Waiting -> text_connection_status.text = getString(R.string.status_waiting, state.seconds)
}
}
}
private val dismissStatus = {
if (connection_status_text != null) {
connection_status_text.fadeOut()
if (text_connection_status != null) {
text_connection_status.fadeOut()
}
}
......@@ -324,4 +324,4 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
return true
}
}
\ No newline at end of file
}
......@@ -5,10 +5,8 @@ import android.text.Spanned
import android.text.TextUtils
import android.util.Base64
import android.util.Patterns
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiTypefaceSpan
import org.json.JSONObject
import ru.noties.markwon.Markwon
import java.net.URLDecoder
......@@ -21,21 +19,6 @@ fun String.ifEmpty(value: String): String {
return this
}
fun CharSequence.ifEmpty(value: String): CharSequence {
if (isEmpty()) {
return value
}
return this
}
fun EditText.erase() {
this.text.clear()
val spans = this.text.getSpans(0, text.length, EmojiTypefaceSpan::class.java)
spans.forEach {
text.removeSpan(it)
}
}
fun String.isEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun String.encodeToBase64(): String {
......@@ -94,9 +77,3 @@ var TextView.content: CharSequence?
Markwon.scheduleDrawables(this)
Markwon.scheduleTableRows(this)
}
var TextView.spanned: CharSequence?
get() = text
set(value) {
text = spanned
}
\ No newline at end of file
......@@ -12,9 +12,9 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/text_typing_status"
app:layout_constraintTop_toBottomOf="@+id/text_connection_status"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintStart_toStartOf="parent">
<include
android:id="@+id/layout_message_list"
......@@ -117,7 +117,7 @@
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<TextView
android:id="@+id/connection_status_text"
android:id="@+id/text_connection_status"
android:layout_width="match_parent"
android:layout_height="32dp"
android:alpha="0"
......@@ -127,6 +127,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorWhite"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
tools:alpha="1"
tools:text="connected"
tools:visibility="visible" />
......@@ -144,4 +145,4 @@
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -8,6 +8,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_below="@+id/text_connection_status"
android:layout_width="match_parent"
android:layout_height="match_parent" />
......@@ -31,7 +32,7 @@
tools:visibility="visible" />
<TextView
android:id="@+id/connection_status_text"
android:id="@+id/text_connection_status"
android:layout_width="match_parent"
android:layout_height="32dp"
android:alpha="0"
......@@ -55,4 +56,4 @@
android:textSize="20sp"
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout>
\ No newline at end of file
</RelativeLayout>
......@@ -267,6 +267,7 @@
<!-- Emoji message-->
<string name="msg_no_recent_emoji">Sin emojis recientes</string>
<string name="alert_title_default_skin_tone">Tono de piel predeterminado</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Ordenar</string>
......
......@@ -269,6 +269,7 @@
<!-- Emoji message-->
<string name="msg_no_recent_emoji">Aucun emoji récent</string>
<string name="alert_title_default_skin_tone">Tonalité de peau par défaut</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Trier</string>
......
......@@ -246,6 +246,7 @@
<!-- Emoji message-->
<string name="msg_no_recent_emoji"> कोई नया इमोजी नहीं</string>
<string name="alert_title_default_skin_tone">डिफ़ॉल्ट त्वचा टोन</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">क्रम</string>
......
......@@ -246,6 +246,7 @@
<!-- Emoji message-->
<string name="msg_no_recent_emoji">Nenhum emoji recente</string>
<string name="alert_title_default_skin_tone">Tom de pele padrão</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Ordenar</string>
......
......@@ -243,6 +243,7 @@
<!-- Emoji message-->
<string name="msg_no_recent_emoji">Нет недавно используемых emoji</string>
<string name="alert_title_default_skin_tone">По умолчанию тон кожи</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Сортировать</string>
......
......@@ -246,6 +246,7 @@
<!-- Emoji message-->
<string name="msg_no_recent_emoji">No recent emojis</string>
<string name="alert_title_default_skin_tone">Default skin tone</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Sort</string>
......
......@@ -29,6 +29,8 @@ dependencies {
implementation libraries.androidKtx
implementation libraries.appCompat
implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.coroutinesAndroid
implementation libraries.constraintlayout
implementation libraries.recyclerview
implementation libraries.material
......
......@@ -22,7 +22,7 @@ class ComposerEditText : AppCompatEditText {
override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
val state = getKeyDispatcherState()
val state = keyDispatcherState
if (state != null) {
if (event.action == KeyEvent.ACTION_DOWN) {
state.startTracking(event, this)
......
......@@ -6,5 +6,7 @@ data class Emoji(
val unicode: String,
val keywords: List<String>,
val category: String,
val count: Int = 0
val count: Int = 0,
val siblings: MutableCollection<Emoji> = mutableListOf(),
val fitzpatrick: Fitzpatrick = Fitzpatrick.Default
)
\ No newline at end of file
package chat.rocket.android.emoji
import android.annotation.SuppressLint
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
......@@ -9,23 +10,32 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.graphics.drawable.DrawableCompat
import androidx.viewpager.widget.ViewPager
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
class EmojiKeyboardPopup(
context: Context,
view: View
) : OverKeyboardPopupWindow(context, view) {
class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow(context, view) {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
private lateinit var searchView: View
private lateinit var backspaceView: View
private lateinit var parentContainer: ViewGroup
private lateinit var changeColorView: View
private lateinit var adapter: EmojiPagerAdapter
var listener: EmojiKeyboardListener? = null
@SuppressLint("InflateParams")
override fun onCreateView(inflater: LayoutInflater): View {
val view = inflater.inflate(R.layout.emoji_keyboard, null)
parentContainer = view.findViewById(R.id.emoji_keyboard_container)
......@@ -33,6 +43,7 @@ class EmojiKeyboardPopup(
searchView = view.findViewById(R.id.emoji_search)
backspaceView = view.findViewById(R.id.emoji_backspace)
tabLayout = view.findViewById(R.id.tabs)
changeColorView = view.findViewById(R.id.color_change_view)
tabLayout.setupWithViewPager(viewPager)
return view
}
......@@ -44,11 +55,97 @@ class EmojiKeyboardPopup(
private fun setupBottomBar() {
searchView.setOnClickListener {
//TODO: search not yet implemented
}
backspaceView.setOnClickListener {
listener?.onNonEmojiKeyPressed(KeyEvent.KEYCODE_BACK)
}
changeColorView.setOnClickListener {
showSkinToneChooser()
}
val sharedPreferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
sharedPreferences.getString(PREF_EMOJI_SKIN_TONE, "")?.let {
changeSkinTone(Fitzpatrick.valueOf(it))
}
}
private fun showSkinToneChooser() {
val view = LayoutInflater.from(context).inflate(R.layout.color_select_popup, null)
val dialog = AlertDialog.Builder(context, R.style.Dialog)
.setView(view)
.setTitle(context.getString(R.string.alert_title_default_skin_tone))
.setCancelable(true)
.create()
view.findViewById<TextView>(R.id.default_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.Default)
}
view.findViewById<TextView>(R.id.light_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.LightTone)
}
view.findViewById<TextView>(R.id.medium_light_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumLightTone)
}
view.findViewById<TextView>(R.id.medium_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumTone)
}
view.findViewById<TextView>(R.id.medium_dark_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumDarkTone)
}
view.findViewById<TextView>(R.id.dark_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.DarkTone)
}
dialog.show()
}
private fun changeSkinTone(tone: Fitzpatrick) {
val drawable = ContextCompat.getDrawable(context, R.drawable.color_change_circle)!!
val wrappedDrawable = DrawableCompat.wrap(drawable)
DrawableCompat.setTint(wrappedDrawable, getFitzpatrickColor(tone))
(changeColorView as ImageView).setImageDrawable(wrappedDrawable)
adapter.setFitzpatrick(tone)
}
@ColorInt
private fun getFitzpatrickColor(tone: Fitzpatrick): Int {
val sharedPreferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
sharedPreferences.edit {
putString(PREF_EMOJI_SKIN_TONE, tone.type)
}
return when (tone) {
Fitzpatrick.Default -> ContextCompat.getColor(context, R.color.tone_default)
Fitzpatrick.LightTone -> ContextCompat.getColor(context, R.color.tone_light)
Fitzpatrick.MediumLightTone -> ContextCompat.getColor(context, R.color.tone_medium_light)
Fitzpatrick.MediumTone -> ContextCompat.getColor(context, R.color.tone_medium)
Fitzpatrick.MediumDarkTone -> ContextCompat.getColor(context, R.color.tone_medium_dark)
Fitzpatrick.DarkTone -> ContextCompat.getColor(context, R.color.tone_dark)
}
}
private fun setupViewPager() {
......@@ -64,12 +161,14 @@ class EmojiKeyboardPopup(
}
}
viewPager.adapter = CategoryPagerAdapter(object : EmojiKeyboardListener {
adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
callback.onEmojiAdded(emoji)
}
})
viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter
for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal)
......@@ -79,14 +178,18 @@ class EmojiKeyboardPopup(
textView.setImageResource(category.resourceIcon())
}
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
val currentTab = if (EmojiRepository.getRecents().isEmpty()) {
EmojiCategory.PEOPLE.ordinal
} else {
EmojiCategory.RECENTS.ordinal
}
viewPager.currentItem = currentTab
}
}
class EmojiTextWatcher(private val editor: EditText) : TextWatcher {
@Volatile private var emojiToRemove = mutableListOf<EmojiTypefaceSpan>()
@Volatile
private var emojiToRemove = mutableListOf<EmojiTypefaceSpan>()
override fun afterTextChanged(s: Editable) {
val message = editor.editableText
......@@ -128,8 +231,4 @@ class EmojiKeyboardPopup(
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
}
}
companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
}
}
\ No newline at end of file
package chat.rocket.android.emoji
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
......@@ -11,11 +12,12 @@ class EmojiParser {
* Spannable.
*
* @param text The text to parse
* @param factory Optional. A [Spannable.Factory] instance to reuse when creating [Spannable].
* @return A rendered Spannable containing any supported emoji.
*/
fun parse(text: CharSequence): CharSequence {
fun parse(text: CharSequence, factory: Spannable.Factory? = null): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannable = SpannableString.valueOf(unicodedText)
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
......
......@@ -3,18 +3,23 @@ package chat.rocket.android.emoji
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import com.google.android.material.tabs.TabLayout
import androidx.viewpager.widget.ViewPager
import android.view.LayoutInflater
import android.view.Window
import android.view.WindowManager
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.content.edit
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.*
class EmojiPickerPopup(context: Context) : Dialog(context) {
var listener: EmojiKeyboardListener? = null
private lateinit var adapter: EmojiPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -35,7 +40,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
}
private fun setupViewPager() {
pager_categories.adapter = CategoryPagerAdapter(object : EmojiKeyboardListener {
adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
dismiss()
......@@ -43,6 +48,14 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
}
})
val sharedPreferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
sharedPreferences.getString(PREF_EMOJI_SKIN_TONE, "")?.let {
changeSkinTone(Fitzpatrick.valueOf(it))
}
pager_categories.adapter = adapter
pager_categories.offscreenPageLimit = EmojiCategory.values().size
for (category in EmojiCategory.values()) {
val tab = tabs.getTabAt(category.ordinal)
val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null)
......@@ -51,8 +64,15 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
textView.setImageResource(category.resourceIcon())
}
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
val currentTab = if (EmojiRepository.getRecents().isEmpty()) {
EmojiCategory.PEOPLE.ordinal
} else {
EmojiCategory.RECENTS.ordinal
}
pager_categories.currentItem = currentTab
}
private fun changeSkinTone(tone: Fitzpatrick) {
adapter.setFitzpatrick(tone)
}
}
\ No newline at end of file
......@@ -3,6 +3,12 @@ package chat.rocket.android.emoji
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Typeface
import android.os.SystemClock
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.experimental.yield
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
......@@ -10,8 +16,11 @@ import java.io.InputStream
import java.io.InputStreamReader
import java.util.*
import java.util.regex.Pattern
import kotlin.coroutines.experimental.buildSequence
object EmojiRepository {
private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE)
private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>()
......@@ -24,9 +33,9 @@ object EmojiRepository {
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
val stream = context.assets.open(path)
val emojis = loadEmojis(stream)
emojis.forEach {
emojis.forEach { emoji ->
val unicodeIntList = mutableListOf<Int>()
it.unicode.split("-").forEach {
emoji.unicode.split("-").forEach {
val value = it.toInt(16)
if (value >= 0x10000) {
val surrogatePair = calculateSurrogatePairs(value)
......@@ -38,20 +47,40 @@ object EmojiRepository {
}
val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
ALL_EMOJIS.add(it.copy(unicode = unicode))
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 {
ALL_EMOJIS.add(emojiWithUnicode)
}
shortNameToUnicode.apply {
put(it.shortname, unicode)
it.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
put(emoji.shortname, unicode)
emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
}
}
}
private fun hasFitzpatrick(shortname: String): Boolean {
return FITZPATRICK_REGEX matches shortname
}
/**
* Get all loaded emojis as list of Emoji objects.
*
* @return All emojis for all categories.
*/
fun getAll() = ALL_EMOJIS
internal fun getAll() = ALL_EMOJIS
/**
* Get all emojis for a given category.
......@@ -60,10 +89,19 @@ object EmojiRepository {
*
* @return All emoji from specified category
*/
fun getEmojisByCategory(category: EmojiCategory): List<Emoji> {
internal fun getEmojisByCategory(category: EmojiCategory): List<Emoji> {
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
}
internal fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> {
val list = ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
return buildSequence{
list.forEach {
yield(it)
}
}
}
/**
* Get the emoji given by a specified shortname. Returns null if can't find any.
*
......@@ -71,21 +109,21 @@ object EmojiRepository {
*
* @return Emoji given by shortname or null
*/
fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname }
internal fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname }
/**
* Add an emoji to the Recents category.
*/
fun addToRecents(emoji: Emoji) {
internal fun addToRecents(emoji: Emoji) {
val emojiShortname = emoji.shortname
val recentsJson = JSONObject(preferences.getString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, "{}"))
val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
if (recentsJson.has(emojiShortname)) {
val useCount = recentsJson.getInt(emojiShortname)
recentsJson.put(emojiShortname, useCount + 1)
} else {
recentsJson.put(emojiShortname, 1)
}
preferences.edit().putString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
preferences.edit().putString(PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
}
/**
......@@ -93,9 +131,9 @@ object EmojiRepository {
*
* @return All recent emojis ordered by usage.
*/
fun getRecents(): List<Emoji> {
internal fun getRecents(): List<Emoji> {
val list = mutableListOf<Emoji>()
val recentsJson = JSONObject(preferences.getString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, "{}"))
val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
for (shortname in recentsJson.keys()) {
val emoji = getEmojiByShortname(shortname)
emoji?.let {
......
......@@ -16,23 +16,6 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
}
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int
val old = paint.typeface
if (old == null) {
oldStyle = 0
} else {
oldStyle = old.style
}
val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.isFakeBoldText = true
}
if (fake and Typeface.ITALIC != 0) {
paint.textSkewX = -0.25f
}
paint.typeface = tf
}
}
\ No newline at end of file
package chat.rocket.android.emoji
/**
* Taken the Fitzpatrick scale as reference and adapted to be used with emojione.
*
* @see <a href="https://en.wikipedia.org/wiki/Fitzpatrick_scale">https://en.wikipedia.org/wiki/Fitzpatrick_scale</a>
*/
sealed class Fitzpatrick(val type: String) {
object Default: Fitzpatrick("")
object LightTone: Fitzpatrick("tone1")
object MediumLightTone: Fitzpatrick("tone2")
object MediumTone: Fitzpatrick("tone3")
object MediumDarkTone: Fitzpatrick("tone4")
object DarkTone: Fitzpatrick("tone5")
companion object {
fun valueOf(type: String): Fitzpatrick {
return when(type) {
"" -> Default
"tone1" -> LightTone
"tone2" -> MediumLightTone
"tone3" -> MediumTone
"tone4" -> MediumDarkTone
"tone5" -> DarkTone
else -> throw IllegalArgumentException("Fitzpatrick type '$type' is invalid")
}
}
}
}
\ No newline at end of file
package chat.rocket.android.emoji
package chat.rocket.android.emoji.internal
import android.text.SpannableString
import android.text.Spanned
import androidx.annotation.DrawableRes
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.EmojiTypefaceSpan
import chat.rocket.android.emoji.R
enum class EmojiCategory {
internal enum class EmojiCategory {
RECENTS {
override fun resourceIcon() = R.drawable.ic_emoji_recents
......
package chat.rocket.android.emoji
package chat.rocket.android.emoji.internal
import android.text.Spannable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.PagerAdapter
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiKeyboardListener
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 kotlinx.android.synthetic.main.emoji_category_layout.view.*
import java.util.*
import kotlinx.android.synthetic.main.emoji_row_item.view.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener) : PagerAdapter() {
internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : PagerAdapter() {
private val adapters = hashMapOf<EmojiCategory, EmojiAdapter>()
private var fitzpatrick: Fitzpatrick = Fitzpatrick.Default
override fun isViewFromObject(view: View, obj: Any): Boolean {
return view == obj
......@@ -23,21 +36,28 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener)
.inflate(R.layout.emoji_category_layout, container, false)
with(view) {
val layoutManager = GridLayoutManager(context, 8)
val adapter = EmojiAdapter(layoutManager.spanCount, listener)
val category = EmojiCategory.values()[position]
val emojis = if (category != EmojiCategory.RECENTS) {
EmojiRepository.getEmojisByCategory(category)
} else {
EmojiRepository.getRecents()
}
val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
adapter.addEmojis(emojis)
emoji_recycler_view.layoutManager = layoutManager
emoji_recycler_view.itemAnimator = DefaultItemAnimator()
emoji_recycler_view.adapter = adapter
emoji_recycler_view.isNestedScrollingEnabled = false
emoji_recycler_view.setRecycledViewPool(RecyclerView.RecycledViewPool())
container.addView(view)
launch(UI) {
val emojis = if (category != EmojiCategory.RECENTS) {
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
}
......@@ -50,25 +70,58 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener)
override fun getPageTitle(position: Int) = EmojiCategory.values()[position].textIcon()
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
fun setFitzpatrick(fitzpatrick: Fitzpatrick) {
this.fitzpatrick = fitzpatrick
for (entry in adapters.entries) {
if (entry.key != EmojiCategory.RECENTS) {
entry.value.setFitzpatrick(fitzpatrick)
}
}
}
class EmojiAdapter(
private val spanCount: Int,
private var fitzpatrick: Fitzpatrick = Fitzpatrick.Default,
private val listener: EmojiKeyboardListener
) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis = Collections.emptyList<Emoji>()
private val emojis = mutableListOf<Emoji>()
fun addEmojis(emojis: List<Emoji>) {
this.emojis = emojis
notifyItemRangeInserted(0, emojis.size)
this.emojis.clear()
this.emojis.addAll(emojis)
notifyDataSetChanged()
}
suspend fun addEmojisFromSequence(emojiSequence: Sequence<Emoji>) {
withContext(CommonPool) {
emojiSequence.forEachIndexed { index, emoji ->
withContext(UI) {
emojis.add(emoji)
notifyItemInserted(index)
}
}
}
}
fun setFitzpatrick(fitzpatrick: Fitzpatrick) {
this.fitzpatrick = fitzpatrick
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
holder.bind(emojis[position])
val emoji = emojis[position]
holder.bind(
emoji.siblings.find { it.fitzpatrick == fitzpatrick } ?: emoji
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false)
return EmojiRowViewHolder(view, itemCount, spanCount, listener)
return EmojiRowViewHolder(view, listener)
}
override fun getItemCount(): Int = emojis.size
......@@ -76,25 +129,30 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener)
class EmojiRowViewHolder(
itemView: View,
private val itemCount: Int,
private val spanCount: Int,
private val listener: EmojiKeyboardListener
) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) {
val context = itemView.context
emojiView.text = EmojiParser.parse(emoji.unicode)
val remainder = itemCount % spanCount
val lastLineItemCount = if (remainder == 0) spanCount else remainder
val paddingBottom = context.resources.getDimensionPixelSize(R.dimen.picker_padding_bottom)
if (adapterPosition >= itemCount - lastLineItemCount) {
itemView.setPadding(0, 0, 0, paddingBottom)
}
itemView.setOnClickListener {
listener.onEmojiAdded(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
}
} else {
parsedUnicode
}
itemView.setOnClickListener {
listener.onEmojiAdded(emoji)
}
}
}
companion object {
private val spannableFactory = Spannable.Factory()
private val unicodeCache = mutableMapOf<CharSequence, CharSequence>()
}
}
}
\ No newline at end of file
package chat.rocket.android.emoji.internal
internal const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
internal const val PREF_EMOJI_SKIN_TONE = "PREF_EMOJI_SKIN_TONE"
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/tone_default" />
<size
android:width="24dp"
android:height="24dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/picker_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorWhite"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@color/whitesmoke"
app:tabGravity="fill"
app:tabMode="scrollable" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/pager_categories"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="@color/colorWhite" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/default_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="👌"
android:textSize="32sp"
android:tint="@color/tone_default"
app:layout_constraintEnd_toStartOf="@+id/light_tone_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle" />
<TextView
android:id="@+id/light_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="👌🏻"
android:textSize="32sp"
android:tint="@color/tone_light"
app:layout_constraintEnd_toStartOf="@+id/medium_light_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/default_tone_text"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/medium_light_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="👌🏼"
android:textSize="32sp"
android:tint="@color/tone_medium_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/light_tone_text"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/medium_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="👌🏽"
android:textSize="32sp"
android:tint="@color/tone_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/medium_dark_tone_text"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/default_tone_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/medium_dark_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:text="👌🏾"
android:textSize="32sp"
android:tint="@color/tone_medium_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/dark_tone_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/medium_tone_text"
app:layout_constraintTop_toBottomOf="@+id/light_tone_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/dark_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="👌🏿"
android:textSize="32sp"
android:tint="@color/tone_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/medium_dark_tone_text"
app:layout_constraintTop_toBottomOf="@+id/medium_light_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
......@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_keyboard_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_height="wrap_content"
android:background="@color/colorWhite">
<View
......@@ -26,7 +26,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/emoji_actions_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
......@@ -36,29 +36,40 @@
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/emoji_search"
android:id="@+id/color_change_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle" />
<ImageView
android:id="@+id/emoji_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/whitesmoke"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:src="@drawable/ic_search_gray_24px"
android:visibility="invisible" />
android:visibility="invisible"
app:layout_constraintEnd_toStartOf="@+id/emoji_backspace"
app:layout_constraintStart_toEndOf="@+id/color_change_view" />
<ImageView
android:id="@+id/emoji_backspace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@color/whitesmoke"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:src="@drawable/ic_backspace_gray_24dp" />
</RelativeLayout>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_backspace_gray_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:id="@+id/emoji_view"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="48dp"
android:layout_height="48dp"
android:foreground="?selectableItemBackground"
android:gravity="center">
<TextView
android:id="@+id/emoji"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#000000"
android:textSize="26sp"
tools:text="😀" />
</FrameLayout>
\ No newline at end of file
android:textColor="#000000"
android:textSize="26sp"
tools:text="😀" />
......@@ -7,4 +7,12 @@
<color name="colorEmojiIcon">#FF767676</color>
<color name="colorDividerMessageComposer">#D8D8D8</color>
<!-- Skin tone colors -->
<color name="tone_default">#fdcb45</color>
<color name="tone_light">#fcd6b4</color>
<color name="tone_medium_light">#ecc0a1</color>
<color name="tone_medium">#ba8b6f</color>
<color name="tone_medium_dark">#926b4f</color>
<color name="tone_dark">#614833</color>
</resources>
<resources>
<string name="msg_no_recent_emoji">No recent emoji</string>
<string name="alert_title_default_skin_tone">Default skin tone</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Dialog" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:windowMinWidthMajor">80%</item>
<item name="android:windowMinWidthMinor">80%</item>
</style>
</resources>
\ 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