Unverified Commit 4e3936ca authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge branch 'develop' into new/google-analytics-for-firebase

parents 8ce5d9d3 9e631abd
...@@ -134,6 +134,8 @@ dependencies { ...@@ -134,6 +134,8 @@ dependencies {
implementation libraries.frescoWebP implementation libraries.frescoWebP
implementation libraries.frescoAnimatedWebP implementation libraries.frescoAnimatedWebP
implementation libraries.glide
kapt libraries.kotshiCompiler kapt libraries.kotshiCompiler
implementation libraries.kotshiApi implementation libraries.kotshiApi
......
...@@ -18,7 +18,6 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor ...@@ -18,7 +18,6 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.SITE_URL import chat.rocket.android.server.domain.SITE_URL
import chat.rocket.android.server.domain.TokenRepository import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.util.setupFabric import chat.rocket.android.util.setupFabric
import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.backends.pipeline.Fresco
...@@ -84,7 +83,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -84,7 +83,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
context = WeakReference(applicationContext) context = WeakReference(applicationContext)
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
EmojiRepository.load(this)
setupFabric(this) setupFabric(this)
setupFresco() setupFresco()
......
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.uimodel.ActionsAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import kotlinx.android.synthetic.main.item_actions_attachment.view.*
import androidx.recyclerview.widget.LinearLayoutManager
import timber.log.Timber
class ActionsAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
var actionAttachmentOnClickListener: ActionAttachmentOnClickListener
) : BaseViewHolder<ActionsAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(actions_attachment_container)
}
}
override fun bindViews(data: ActionsAttachmentUiModel) {
val actions = data.actions
val alignment = data.buttonAlignment
Timber.d("no of actions : ${actions.size} : $actions")
with(itemView) {
title.text = data.title ?: ""
actions_list.layoutManager = LinearLayoutManager(itemView.context,
when (alignment) {
"horizontal" -> LinearLayoutManager.HORIZONTAL
else -> LinearLayoutManager.VERTICAL //Default
}, false)
actions_list.adapter = ActionsListAdapter(actions, actionAttachmentOnClickListener)
}
}
}
interface ActionAttachmentOnClickListener {
fun onActionClicked(view: View, action: Action)
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.util.extensions.inflate
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.item_action_button.view.*
import timber.log.Timber
class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListener: ActionAttachmentOnClickListener) : RecyclerView.Adapter<ActionsListAdapter.ViewHolder>() {
var actions: List<Action> = actions
inner class ViewHolder(var layout: View) : RecyclerView.ViewHolder(layout) {
lateinit var action: ButtonAction
private val onClickListener = View.OnClickListener {
actionAttachmentOnClickListener.onActionClicked(it, action)
}
init {
with(itemView) {
action_button.setOnClickListener(onClickListener)
action_image_button.setOnClickListener(onClickListener)
}
}
fun bindAction(action: Action) {
with(itemView) {
Timber.d("action : $action")
this@ViewHolder.action = action as ButtonAction
if (action.imageUrl != null) {
action_button.isVisible = false
action_image_button.isVisible = true
//Image button
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(action.imageUrl)
autoPlayAnimations = true
oldController = action_image_button.controller
}.build()
action_image_button.controller = controller
} else if (action.text != null) {
action_button.isVisible = true
action_image_button.isVisible = false
this.action_button.setText(action.text)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = parent.inflate(R.layout.item_action_button)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return actions.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val action = actions[position]
holder.bindAction(action)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.app.AlertDialog
import android.content.Context
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.uimodel.* import chat.rocket.android.chatroom.uimodel.*
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.emoji.EmojiReactionListener import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage import chat.rocket.core.model.isSystemMessage
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
class ChatRoomAdapter( class ChatRoomAdapter(
private val roomId: String? = null,
private val roomType: String? = null, private val roomType: String? = null,
private val roomName: String? = null, private val roomName: String? = null,
private val actionSelectListener: OnActionSelected? = null, private val actionSelectListener: OnActionSelected? = null,
...@@ -72,6 +74,10 @@ class ChatRoomAdapter( ...@@ -72,6 +74,10 @@ class ChatRoomAdapter(
actionSelectListener?.openDirectMessage(roomName, permalink) actionSelectListener?.openDirectMessage(roomName, permalink)
} }
} }
BaseUiModel.ViewType.ACTIONS_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_actions_attachment)
ActionsAttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -125,6 +131,8 @@ class ChatRoomAdapter( ...@@ -125,6 +131,8 @@ class ChatRoomAdapter(
holder.bind(dataSet[position] as GenericFileAttachmentUiModel) holder.bind(dataSet[position] as GenericFileAttachmentUiModel)
is MessageReplyViewHolder -> is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel) holder.bind(dataSet[position] as MessageReplyUiModel)
is ActionsAttachmentViewHolder ->
holder.bind(dataSet[position] as ActionsAttachmentUiModel)
} }
} }
...@@ -203,6 +211,33 @@ class ChatRoomAdapter( ...@@ -203,6 +211,33 @@ class ChatRoomAdapter(
} }
} }
private val actionAttachmentOnClickListener = object : ActionAttachmentOnClickListener {
override fun onActionClicked(view: View, action: Action) {
val temp = action as ButtonAction
if (temp.url != null && temp.isWebView != null) {
if (temp.isWebView == true) {
//TODO: Open in a configurable sizable webview
Timber.d("Open in a configurable sizable webview")
} else {
//Open in chrome custom tab
temp.url?.let { view.openTabbedUrl(it) }
}
} else if (temp.message != null && temp.isMessageInChatWindow != null) {
if (temp.isMessageInChatWindow == true) {
//Send to chat window
temp.message?.let {
if (roomId != null) {
actionSelectListener?.sendMessage(roomId, it)
}
}
} else {
//TODO: Send to bot but not in chat window
Timber.d("Send to bot but not in chat window")
}
}
}
}
private val actionsListener = object : BaseViewHolder.ActionsListener { private val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions override fun isActionsEnabled(): Boolean = enableActions
...@@ -259,5 +294,6 @@ class ChatRoomAdapter( ...@@ -259,5 +294,6 @@ class ChatRoomAdapter(
fun deleteMessage(roomId: String, id: String) fun deleteMessage(roomId: String, id: String)
fun showReactions(id: String) fun showReactions(id: String)
fun openDirectMessage(roomName: String, message: String) fun openDirectMessage(roomName: String, message: String)
fun sendMessage(chatRoomId: String, text: String)
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.style.ImageSpan
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.MessageUiModel import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.emoji.EmojiReactionListener import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.isSystemMessage 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.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.* import kotlinx.android.synthetic.main.item_message.view.*
...@@ -15,7 +19,7 @@ class MessageViewHolder( ...@@ -15,7 +19,7 @@ class MessageViewHolder(
itemView: View, itemView: View,
listener: ActionsListener, listener: ActionsListener,
reactionListener: EmojiReactionListener? = null reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener) { ) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
init { init {
with(itemView) { with(itemView) {
...@@ -26,22 +30,26 @@ class MessageViewHolder( ...@@ -26,22 +30,26 @@ class MessageViewHolder(
override fun bindViews(data: MessageUiModel) { override fun bindViews(data: MessageUiModel) {
with(itemView) { with(itemView) {
day_marker_layout.visibility = if (data.showDayMarker) { day.text = data.currentDayMarkerText
day.text = data.currentDayMarkerText day_marker_layout.isVisible = data.showDayMarker
View.VISIBLE
} else {
View.GONE
}
if (data.isFirstUnread) { new_messages_notif.isVisible = data.isFirstUnread
new_messages_notif.visibility = View.VISIBLE
} else {
new_messages_notif.visibility = View.GONE
}
text_message_time.text = data.time text_message_time.text = data.time
text_sender.text = data.senderName 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) image_avatar.setImageURI(data.avatar)
text_content.setTextColor(if (data.isTemporary) Color.GRAY else Color.BLACK) text_content.setTextColor(if (data.isTemporary) Color.GRAY else Color.BLACK)
...@@ -64,4 +72,22 @@ class MessageViewHolder( ...@@ -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 { ...@@ -148,9 +148,4 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean) 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 ...@@ -6,9 +6,13 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.SystemClock
import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.ImageSpan
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
...@@ -53,11 +57,13 @@ import chat.rocket.android.emoji.EmojiKeyboardPopup ...@@ -53,11 +57,13 @@ import chat.rocket.android.emoji.EmojiKeyboardPopup
import chat.rocket.android.emoji.EmojiParser import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiPickerPopup import chat.rocket.android.emoji.EmojiPickerPopup
import chat.rocket.android.emoji.EmojiReactionListener import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.emoji.internal.isCustom
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.ImageHelper import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extension.asObservable 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.circularRevealOrUnreveal
import chat.rocket.android.util.extensions.fadeIn import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut import chat.rocket.android.util.extensions.fadeOut
...@@ -71,6 +77,7 @@ import chat.rocket.common.model.RoomType ...@@ -71,6 +77,7 @@ import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import com.bumptech.glide.load.resource.gif.GifDrawable
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
...@@ -79,6 +86,8 @@ import kotlinx.android.synthetic.main.fragment_chat_room.* ...@@ -79,6 +86,8 @@ import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.* import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.* import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.* 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.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject import javax.inject.Inject
...@@ -131,7 +140,8 @@ internal const val MENU_ACTION_FAVORITE_MESSAGES = 5 ...@@ -131,7 +140,8 @@ internal const val MENU_ACTION_FAVORITE_MESSAGES = 5
internal const val MENU_ACTION_FILES = 6 internal const val MENU_ACTION_FILES = 6
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener, class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener,
ChatRoomAdapter.OnActionSelected { ChatRoomAdapter.OnActionSelected, Drawable.Callback {
@Inject @Inject
lateinit var presenter: ChatRoomPresenter lateinit var presenter: ChatRoomPresenter
@Inject @Inject
...@@ -208,10 +218,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -208,10 +218,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
} }
adapter = ChatRoomAdapter( adapter = ChatRoomAdapter(chatRoomId, chatRoomType, chatRoomName, this, reactionListener = this)
chatRoomType, chatRoomName, this,
reactionListener = this
)
} }
override fun onCreateView( override fun onCreateView(
...@@ -395,10 +402,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -395,10 +402,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) {
}
private val layoutChangeListener = private val layoutChangeListener =
View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom -> View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
val y = oldBottom - bottom val y = oldBottom - bottom
...@@ -641,8 +644,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -641,8 +644,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
val cursorPosition = text_message.selectionStart val cursorPosition = text_message.selectionStart
if (cursorPosition > -1) { if (cursorPosition > -1) {
text_message.text?.insert(cursorPosition, EmojiParser.parse(emoji.shortname)) context?.let {
text_message.setSelection(cursorPosition + emoji.unicode.length) 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)
}
} }
} }
...@@ -773,9 +780,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -773,9 +780,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
button_send.isVisible = false button_send.isVisible = false
button_show_attachment_options.alpha = 1f button_show_attachment_options.alpha = 1f
button_show_attachment_options.isVisible = true button_show_attachment_options.isVisible = true
activity?.supportFragmentManager?.addOnBackStackChangedListener { activity?.supportFragmentManager?.addOnBackStackChangedListener {
println("attach") println("attach")
} }
activity?.supportFragmentManager?.registerFragmentLifecycleCallbacks( activity?.supportFragmentManager?.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() { object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached( override fun onFragmentAttached(
...@@ -791,6 +800,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -791,6 +800,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}, },
true true
) )
subscribeComposeTextMessage() subscribeComposeTextMessage()
emojiKeyboardPopup = emojiKeyboardPopup =
EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container)) EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
...@@ -979,6 +989,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -979,6 +989,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
(activity as ChatRoomActivity).showToolbarTitle(toolbarTitle) (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) { override fun showMessageInfo(id: String) {
presenter.messageInfo(id) presenter.messageInfo(id)
} }
...@@ -1039,4 +1061,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -1039,4 +1061,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun openDirectMessage(roomName: String, message: String) { override fun openDirectMessage(roomName: String, message: String) {
presenter.openDirectMessage(roomName, message) presenter.openDirectMessage(roomName, message)
} }
override fun sendMessage(chatRoomId: String, text: String) {
presenter.sendMessage(chatRoomId, text, null)
}
} }
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ActionsAttachment
data class ActionsAttachmentUiModel(
override val attachmentUrl: String,
val title: String?,
val actions: List<Action>,
val buttonAlignment: String,
override val message: Message,
override val rawData: ActionsAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<ActionsAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.ACTIONS_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_actions_attachment
}
\ No newline at end of file
...@@ -29,7 +29,8 @@ interface BaseUiModel<out T> { ...@@ -29,7 +29,8 @@ interface BaseUiModel<out T> {
AUTHOR_ATTACHMENT(7), AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8), COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9), GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10) MESSAGE_REPLY(10),
ACTIONS_ATTACHMENT(11)
} }
} }
......
...@@ -46,6 +46,7 @@ import chat.rocket.core.model.attachment.GenericFileAttachment ...@@ -46,6 +46,7 @@ import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.isSystemMessage import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
...@@ -305,10 +306,26 @@ class UiModelMapper @Inject constructor( ...@@ -305,10 +306,26 @@ class UiModelMapper @Inject constructor(
is MessageAttachment -> mapMessageAttachment(message, attachment) is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment) is AuthorAttachment -> mapAuthorAttachment(message, attachment)
is ColorAttachment -> mapColorAttachment(message, attachment) is ColorAttachment -> mapColorAttachment(message, attachment)
is ActionsAttachment -> mapActionsAttachment(message, attachment)
else -> null else -> null
} }
} }
private fun mapActionsAttachment(message: Message, attachment: ActionsAttachment): BaseUiModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
ActionsAttachmentUiModel(attachmentUrl = url, title = title,
actions = actions, buttonAlignment = buttonAlignment, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
}
}
private fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseUiModel<*>? { private fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseUiModel<*>? {
return with(attachment) { return with(attachment) {
val content = stripMessageQuotes(message) val content = stripMessageQuotes(message)
...@@ -493,7 +510,7 @@ class UiModelMapper @Inject constructor( ...@@ -493,7 +510,7 @@ class UiModelMapper @Inject constructor(
list.add( list.add(
ReactionUiModel(messageId = message.id, ReactionUiModel(messageId = message.id,
shortname = shortname, shortname = shortname,
unicode = EmojiParser.parse(shortname), unicode = EmojiParser.parse(context, shortname),
count = count, count = count,
usernames = usernames) usernames = usernames)
) )
......
...@@ -7,6 +7,7 @@ import android.graphics.Paint ...@@ -7,6 +7,7 @@ import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.text.Spanned import android.text.Spanned
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.text.style.ImageSpan
import android.text.style.ReplacementSpan import android.text.style.ReplacementSpan
import android.util.Patterns import android.util.Patterns
import android.view.View import android.view.View
...@@ -25,7 +26,6 @@ import org.commonmark.node.Document ...@@ -25,7 +26,6 @@ import org.commonmark.node.Document
import org.commonmark.node.ListItem import org.commonmark.node.ListItem
import org.commonmark.node.Node import org.commonmark.node.Node
import org.commonmark.node.OrderedList import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.Text import org.commonmark.node.Text
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder import ru.noties.markwon.SpannableBuilder
...@@ -60,11 +60,11 @@ class MessageParser @Inject constructor( ...@@ -60,11 +60,11 @@ class MessageParser @Inject constructor(
} }
} }
val builder = SpannableBuilder() val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text, true) val content = EmojiRepository.shortnameToUnicode(text)
val parentNode = parser.parse(toLenientMarkdown(content)) val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(MarkdownVisitor(configuration, builder)) parentNode.accept(MarkdownVisitor(configuration, builder))
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(configuration, builder)) parentNode.accept(EmojiVisitor(context, configuration, builder))
message.mentions?.let { message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername)) parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername))
} }
...@@ -126,16 +126,29 @@ class MessageParser @Inject constructor( ...@@ -126,16 +126,29 @@ class MessageParser @Inject constructor(
} }
class EmojiVisitor( class EmojiVisitor(
private val context: Context,
configuration: SpannableConfiguration, configuration: SpannableConfiguration,
private val builder: SpannableBuilder private val builder: SpannableBuilder
) : SpannableMarkdownVisitor(configuration, builder) { ) : SpannableMarkdownVisitor(configuration, builder) {
private val emojiSize = context.resources.getDimensionPixelSize(R.dimen.radius_mention)
override fun visit(document: Document) { override fun visit(document: Document) {
val spannable = EmojiParser.parse(builder.text()) val spannable = EmojiParser.parse(context, builder.text())
if (spannable is Spanned) { if (spannable is Spanned) {
val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java) val emojiOneTypefaceSpans = spannable.getSpans(0, spannable.length,
spans.forEach { EmojiTypefaceSpan::class.java)
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0) 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( ...@@ -230,4 +243,4 @@ class MessageParser @Inject constructor(
canvas.drawText(text, start, end, x + padding, y.toFloat(), paint) canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory 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.infrastructure.LocalRepository
import chat.rocket.android.main.uimodel.NavHeaderUiModel import chat.rocket.android.main.uimodel.NavHeaderUiModel
import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper
...@@ -30,6 +35,7 @@ import chat.rocket.common.RocketChatException ...@@ -30,6 +35,7 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient 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.logout
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken import chat.rocket.core.internal.rest.unregisterPushToken
...@@ -125,6 +131,38 @@ class MainPresenter @Inject constructor( ...@@ -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. * Logout from current server.
*/ */
......
...@@ -76,6 +76,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -76,6 +76,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
presenter.connect() presenter.connect()
presenter.loadServerAccounts() presenter.loadServerAccounts()
presenter.loadCurrentInfo() presenter.loadCurrentInfo()
presenter.loadEmojis()
setupToolbar() setupToolbar()
setupNavigationView() setupNavigationView()
} }
......
...@@ -66,12 +66,13 @@ var TextView.content: CharSequence? ...@@ -66,12 +66,13 @@ var TextView.content: CharSequence?
Markwon.unscheduleDrawables(this) Markwon.unscheduleDrawables(this)
Markwon.unscheduleTableRows(this) Markwon.unscheduleTableRows(this)
if (value is Spanned) { 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 val end = if (value.length > result.length) result.length else value.length
TextUtils.copySpansFrom(value, 0, end, Any::class.java, result, 0) TextUtils.copySpansFrom(value, 0, end, Any::class.java, result, 0)
text = result text = result
} else { } else {
val result = EmojiParser.parse(value.toString()) as Spannable val result = EmojiParser.parse(context, value.toString()) as Spannable
text = result text = result
} }
Markwon.scheduleDrawables(this) Markwon.scheduleDrawables(this)
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/action_button"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
android:layout_marginStart="2dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textSize="12sp" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/action_image_button"
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginBottom="10dp"
android:layout_marginEnd="2dp"
android:layout_marginStart="2dp"
android:visibility="gone"
fresco:actualImageScaleType="fitStart"
fresco:placeholderImage="@drawable/image_dummy" />
</RelativeLayout>
\ 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:id="@+id/actions_attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding">
<TextView
android:id="@+id/title"
style="@style/Message.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginStart="56dp"
android:layout_marginTop="2dp"
android:textDirection="locale"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:background="@drawable/quote_vertical_gray_bar"
app:layout_constraintBottom_toTopOf="@id/recycler_view_reactions"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/actions_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorAccent"
android:textDirection="locale"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toEndOf="@id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/title" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/actions_list" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
<dimen name="supposed_keyboard_height">252dp</dimen> <dimen name="supposed_keyboard_height">252dp</dimen>
<dimen name="picker_popup_height">250dp</dimen> <dimen name="picker_popup_height">250dp</dimen>
<dimen name="picker_popup_width">300dp</dimen> <dimen name="picker_popup_width">300dp</dimen>
<dimen name="emoji_size">22dp</dimen>
<!--Toolbar--> <!--Toolbar-->
<dimen name="toolbar_height">56dp</dimen> <dimen name="toolbar_height">56dp</dimen>
......
...@@ -10,7 +10,7 @@ buildscript { ...@@ -10,7 +10,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.3.0-alpha05' classpath 'com.android.tools.build:gradle:3.2.0-rc02'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:4.0.2' classpath 'com.google.gms:google-services:4.0.2'
......
...@@ -5,7 +5,7 @@ ext { ...@@ -5,7 +5,7 @@ ext {
compileSdk : 28, compileSdk : 28,
targetSdk : 28, targetSdk : 28,
minSdk : 21, minSdk : 21,
buildTools : '28.0.1', buildTools : '28.0.2',
dokka : '0.9.16', dokka : '0.9.16',
// For app // For app
...@@ -47,6 +47,7 @@ ext { ...@@ -47,6 +47,7 @@ ext {
frescoImageViewer : '0.5.1', frescoImageViewer : '0.5.1',
markwon : '1.1.0', markwon : '1.1.0',
aVLoadingIndicatorView: '2.1.3', aVLoadingIndicatorView: '2.1.3',
glide : '4.8.0-SNAPSHOT',
// For wearable // For wearable
wear : '2.3.0', wear : '2.3.0',
...@@ -106,6 +107,8 @@ ext { ...@@ -106,6 +107,8 @@ ext {
kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}", kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}",
frescoImageViewer : "com.github.luciofm:FrescoImageViewer:${versions.frescoImageViewer}", 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}", markwon : "ru.noties:markwon:${versions.markwon}",
......
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion versions.compileSdk compileSdkVersion versions.compileSdk
...@@ -14,6 +15,11 @@ android { ...@@ -14,6 +15,11 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
buildTypes { buildTypes {
...@@ -34,6 +40,10 @@ dependencies { ...@@ -34,6 +40,10 @@ dependencies {
implementation libraries.constraintlayout implementation libraries.constraintlayout
implementation libraries.recyclerview implementation libraries.recyclerview
implementation libraries.material implementation libraries.material
implementation libraries.glide
kapt libraries.glideProcessor
implementation libraries.room
kapt libraries.roomProcessor
} }
kotlin { kotlin {
......
...@@ -19,3 +19,12 @@ ...@@ -19,3 +19,12 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-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 package chat.rocket.android.emoji
import android.content.Context import android.content.Context
import androidx.appcompat.widget.AppCompatEditText import android.text.Spanned
import android.text.style.ImageSpan
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.text.getSpans
class ComposerEditText : AppCompatEditText { class ComposerEditText : AppCompatEditText {
var listener: ComposerEditTextListener? = null var listener: ComposerEditTextListener? = null
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
super(context, attrs, defStyleAttr) { super(context, attrs, defStyleAttr) {
isFocusable = true isFocusable = true
isFocusableInTouchMode = true isFocusableInTouchMode = true
isClickable = true isClickable = true
...@@ -20,6 +24,21 @@ class ComposerEditText : AppCompatEditText { ...@@ -20,6 +24,21 @@ class ComposerEditText : AppCompatEditText {
constructor(context: Context) : this(context, null) 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 { override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) { if (event.keyCode == KeyEvent.KEYCODE_BACK) {
val state = keyDispatcherState val state = keyDispatcherState
...@@ -43,4 +62,4 @@ class ComposerEditText : AppCompatEditText { ...@@ -43,4 +62,4 @@ class ComposerEditText : AppCompatEditText {
fun onKeyboardClosed() fun onKeyboardClosed()
fun onKeyboardOpened() fun onKeyboardOpened()
} }
} }
\ No newline at end of file
package chat.rocket.android.emoji package chat.rocket.android.emoji
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity
data class Emoji( data class Emoji(
val shortname: String, @PrimaryKey
val shortnameAlternates: List<String>, var shortname: String = "",
val unicode: String, var shortnameAlternates: List<String> = listOf(),
val keywords: List<String>, var unicode: String = "",
val category: String, @Ignore val keywords: List<String> = listOf(),
val count: Int = 0, var category: String = "",
val siblings: MutableCollection<Emoji> = mutableListOf(), var count: Int = 0,
val fitzpatrick: Fitzpatrick = Fitzpatrick.Default var siblings: MutableList<String> = mutableListOf(), // Siblings are the same emoji with different skin tones.
) var fitzpatrick: String = Fitzpatrick.Default.type,
\ No newline at end of file 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 ...@@ -22,6 +22,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import com.google.android.material.tabs.TabLayout 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) { class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow(context, view) {
...@@ -49,8 +51,10 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -49,8 +51,10 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} }
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
setupViewPager() launch(UI) {
setupBottomBar() setupViewPager()
setupBottomBar()
}
} }
private fun setupBottomBar() { private fun setupBottomBar() {
...@@ -81,42 +85,42 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -81,42 +85,42 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
.create() .create()
view.findViewById<TextView>(R.id.default_tone_text).also { view.findViewById<TextView>(R.id.default_tone_text).also {
it.text = EmojiParser.parse(it.text) it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener { }.setOnClickListener {
dialog.dismiss() dialog.dismiss()
changeSkinTone(Fitzpatrick.Default) changeSkinTone(Fitzpatrick.Default)
} }
view.findViewById<TextView>(R.id.light_tone_text).also { view.findViewById<TextView>(R.id.light_tone_text).also {
it.text = EmojiParser.parse(it.text) it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener { }.setOnClickListener {
dialog.dismiss() dialog.dismiss()
changeSkinTone(Fitzpatrick.LightTone) changeSkinTone(Fitzpatrick.LightTone)
} }
view.findViewById<TextView>(R.id.medium_light_text).also { view.findViewById<TextView>(R.id.medium_light_text).also {
it.text = EmojiParser.parse(it.text) it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener { }.setOnClickListener {
dialog.dismiss() dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumLightTone) changeSkinTone(Fitzpatrick.MediumLightTone)
} }
view.findViewById<TextView>(R.id.medium_tone_text).also { view.findViewById<TextView>(R.id.medium_tone_text).also {
it.text = EmojiParser.parse(it.text) it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener { }.setOnClickListener {
dialog.dismiss() dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumTone) changeSkinTone(Fitzpatrick.MediumTone)
} }
view.findViewById<TextView>(R.id.medium_dark_tone_text).also { view.findViewById<TextView>(R.id.medium_dark_tone_text).also {
it.text = EmojiParser.parse(it.text) it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener { }.setOnClickListener {
dialog.dismiss() dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumDarkTone) changeSkinTone(Fitzpatrick.MediumDarkTone)
} }
view.findViewById<TextView>(R.id.dark_tone_text).also { view.findViewById<TextView>(R.id.dark_tone_text).also {
it.text = EmojiParser.parse(it.text) it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener { }.setOnClickListener {
dialog.dismiss() dialog.dismiss()
changeSkinTone(Fitzpatrick.DarkTone) changeSkinTone(Fitzpatrick.DarkTone)
...@@ -148,7 +152,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -148,7 +152,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} }
} }
private fun setupViewPager() { private suspend fun setupViewPager() {
context.let { context.let {
val callback = when (it) { val callback = when (it) {
is EmojiKeyboardListener -> it is EmojiKeyboardListener -> it
...@@ -167,6 +171,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -167,6 +171,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
callback.onEmojiAdded(emoji) callback.onEmojiAdded(emoji)
} }
}) })
viewPager.offscreenPageLimit = EmojiCategory.values().size viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter viewPager.adapter = adapter
...@@ -183,6 +188,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -183,6 +188,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
} else { } else {
EmojiCategory.RECENTS.ordinal EmojiCategory.RECENTS.ordinal
} }
viewPager.currentItem = currentTab viewPager.currentItem = currentTab
} }
} }
......
package chat.rocket.android.emoji package chat.rocket.android.emoji
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Typeface
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned 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 { class EmojiParser {
companion object { companion object {
private val regex = ":[\\w]+:".toRegex()
/** /**
* Parses a text string containing unicode characters and/or shortnames to a rendered * Parses a text string containing unicode characters and/or shortnames to a rendered
* Spannable. * Spannable.
...@@ -15,10 +29,18 @@ class EmojiParser { ...@@ -15,10 +29,18 @@ class EmojiParser {
* @param factory Optional. A [Spannable.Factory] instance to reuse when creating [Spannable]. * @param factory Optional. A [Spannable.Factory] instance to reuse when creating [Spannable].
* @return A rendered Spannable containing any supported emoji. * @return A rendered Spannable containing any supported emoji.
*/ */
fun parse(text: CharSequence, factory: Spannable.Factory? = null): CharSequence { fun parse(context: Context, text: CharSequence, factory: Spannable.Factory? = null): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true) val unicodedText = EmojiRepository.shortnameToUnicode(text)
val spannable = factory?.newSpannable(unicodedText) ?: SpannableString.valueOf(unicodedText) val spannable = factory?.newSpannable(unicodedText)
val typeface = EmojiRepository.cachedTypeface ?: 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. // Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length val length = spannable.length
var inEmoji = false var inEmoji = false
...@@ -32,6 +54,7 @@ class EmojiParser { ...@@ -32,6 +54,7 @@ class EmojiParser {
offset += count offset += count
continue continue
} }
if (codepoint >= 0x200) { if (codepoint >= 0x200) {
if (!inEmoji) { if (!inEmoji) {
emojiStart = offset emojiStart = offset
...@@ -40,17 +63,64 @@ class EmojiParser { ...@@ -40,17 +63,64 @@ class EmojiParser {
} else { } else {
if (inEmoji) { if (inEmoji) {
spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface), spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
inEmoji = false inEmoji = false
} }
offset += count offset += count
if (offset >= length && inEmoji) { if (offset >= length && inEmoji) {
spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface), 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 ...@@ -14,6 +14,8 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import kotlinx.android.synthetic.main.emoji_picker.* import kotlinx.android.synthetic.main.emoji_picker.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
class EmojiPickerPopup(context: Context) : Dialog(context) { class EmojiPickerPopup(context: Context) : Dialog(context) {
...@@ -27,8 +29,10 @@ class EmojiPickerPopup(context: Context) : Dialog(context) { ...@@ -27,8 +29,10 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
setContentView(R.layout.emoji_picker) setContentView(R.layout.emoji_picker)
tabs.setupWithViewPager(pager_categories) tabs.setupWithViewPager(pager_categories)
setupViewPager() launch(UI) {
setSize() setupViewPager()
setSize()
}
} }
private fun setSize() { private fun setSize() {
...@@ -39,7 +43,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) { ...@@ -39,7 +43,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
window.setLayout(dialogWidth, dialogHeight) window.setLayout(dialogWidth, dialogHeight)
} }
private fun setupViewPager() { private suspend fun setupViewPager() {
adapter = EmojiPagerAdapter(object : EmojiKeyboardListener { adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji) EmojiRepository.addToRecents(emoji)
......
...@@ -3,12 +3,14 @@ package chat.rocket.android.emoji ...@@ -3,12 +3,14 @@ package chat.rocket.android.emoji
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Typeface import android.graphics.Typeface
import android.os.SystemClock
import chat.rocket.android.emoji.internal.EmojiCategory import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS
import chat.rocket.android.emoji.internal.db.EmojiDatabase
import chat.rocket.android.emoji.internal.isCustom
import com.bumptech.glide.Glide
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.experimental.yield
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader import java.io.BufferedReader
...@@ -16,61 +18,112 @@ import java.io.InputStream ...@@ -16,61 +18,112 @@ import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.collections.ArrayList
import kotlin.coroutines.experimental.buildSequence import kotlin.coroutines.experimental.buildSequence
object EmojiRepository { object EmojiRepository {
private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE) private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE)
private val shortNameToUnicode = HashMap<String, String>() private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):") private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>() private var customEmojis = listOf<Emoji>()
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
internal lateinit var cachedTypeface: Typeface internal lateinit var cachedTypeface: Typeface
private lateinit var db: EmojiDatabase
private lateinit var currentServerUrl: String
fun setCurrentServerUrl(url: String) {
currentServerUrl = url
}
fun getCurrentServerUrl(): String? {
return if (::currentServerUrl.isInitialized) currentServerUrl else null
}
fun load(context: Context, customEmojis: List<Emoji> = emptyList(), path: String = "emoji.json") {
launch(CommonPool) {
this@EmojiRepository.customEmojis = customEmojis
val allEmojis = mutableListOf<Emoji>()
db = EmojiDatabase.getInstance(context)
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
val stream = context.assets.open(path)
// Load emojis from emojione ttf file temporarily here. We still need to work on them.
val emojis = loadEmojis(stream).also {
it.addAll(customEmojis)
}.toList()
for (emoji in emojis) {
val unicodeIntList = mutableListOf<Int>()
fun load(context: Context, path: String = "emoji.json") { emoji.category = emoji.category
preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
ALL_EMOJIS.clear() if (emoji.isCustom()) {
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf") allEmojis.add(emoji)
val stream = context.assets.open(path) continue
val emojis = loadEmojis(stream)
emojis.forEach { emoji ->
val unicodeIntList = mutableListOf<Int>()
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() emoji.unicode.split("-").forEach {
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size) val value = it.toInt(16)
val emojiWithUnicode = emoji.copy(unicode = unicode) if (value >= 0x10000) {
if (hasFitzpatrick(emoji.shortname)) { val surrogatePair = calculateSurrogatePairs(value)
val matchResult = FITZPATRICK_REGEX.find(emoji.shortname) unicodeIntList.add(surrogatePair.first)
val prefix = matchResult!!.groupValues[1] + ":" unicodeIntList.add(surrogatePair.second)
val fitzpatrick = Fitzpatrick.valueOf(matchResult.groupValues[2]) } else {
val defaultEmoji = ALL_EMOJIS.firstOrNull { it.shortname == prefix } unicodeIntList.add(value)
val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick) }
if (defaultEmoji != null) { }
defaultEmoji.siblings.add(emojiWithFitzpatrick)
} else { val unicodeIntArray = unicodeIntList.toIntArray()
// This emoji doesn't have a default tone, ie. :man_in_business_suit_levitating_tone1: val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
// In this case, the default emoji becomes the first toned one. emoji.unicode = unicode
ALL_EMOJIS.add(emojiWithFitzpatrick)
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 = allEmojis.firstOrNull { it.shortname == prefix }
emoji.fitzpatrick = fitzpatrick.type
emoji.isDefault = if (defaultEmoji != null) {
defaultEmoji.siblings.add(emoji.shortname)
false
} else {
true
}
emoji.isDefault = false
}
allEmojis.add(emoji)
shortNameToUnicode.apply {
put(emoji.shortname, unicode)
emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
} }
} else {
ALL_EMOJIS.add(emojiWithUnicode)
} }
shortNameToUnicode.apply {
put(emoji.shortname, unicode) saveEmojisToDatabase(allEmojis.toList())
emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
// Prefetch all custom emojis to make cache.
val px = context.resources.getDimensionPixelSize(R.dimen.custom_emoji_large)
customEmojis.forEach {
val future = Glide.with(context)
.load(it.url)
.submit(px, px)
future.get()
} }
} }
} }
private suspend fun saveEmojisToDatabase(emojis: List<Emoji>) {
withContext(CommonPool) {
db.emojiDao().insertAllEmojis(*emojis.toTypedArray())
}
}
private fun hasFitzpatrick(shortname: String): Boolean { private fun hasFitzpatrick(shortname: String): Boolean {
return FITZPATRICK_REGEX matches shortname return FITZPATRICK_REGEX matches shortname
} }
...@@ -80,22 +133,28 @@ object EmojiRepository { ...@@ -80,22 +133,28 @@ object EmojiRepository {
* *
* @return All emojis for all categories. * @return All emojis for all categories.
*/ */
internal fun getAll() = ALL_EMOJIS internal suspend fun getAll(): List<Emoji> = withContext(CommonPool) {
return@withContext db.emojiDao().loadAllEmojis()
}
/** internal suspend fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> {
* Get all emojis for a given category. val list = withContext(CommonPool) {
* db.emojiDao().loadEmojisByCategory(category.name)
* @param category Emoji category such as: PEOPLE, NATURE, ETC }
*
* @return All emoji from specified category return buildSequence {
*/ list.forEach {
internal fun getEmojisByCategory(category: EmojiCategory): List<Emoji> { yield(it)
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() } }
}
} }
internal fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> { internal suspend fun getEmojiSequenceByCategoryAndUrl(category: EmojiCategory, url: String): Sequence<Emoji> {
val list = ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() } val list = withContext(CommonPool) {
return buildSequence{ db.emojiDao().loadEmojisByCategoryAndUrl(category.name, "$url%")
}
return buildSequence {
list.forEach { list.forEach {
yield(it) yield(it)
} }
...@@ -109,7 +168,9 @@ object EmojiRepository { ...@@ -109,7 +168,9 @@ object EmojiRepository {
* *
* @return Emoji given by shortname or null * @return Emoji given by shortname or null
*/ */
internal fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname } private suspend fun getEmojiByShortname(shortname: String): Emoji? = withContext(CommonPool) {
return@withContext db.emojiDao().loadAllCustomEmojis().firstOrNull()
}
/** /**
* Add an emoji to the Recents category. * Add an emoji to the Recents category.
...@@ -117,40 +178,68 @@ object EmojiRepository { ...@@ -117,40 +178,68 @@ object EmojiRepository {
internal fun addToRecents(emoji: Emoji) { internal fun addToRecents(emoji: Emoji) {
val emojiShortname = emoji.shortname val emojiShortname = emoji.shortname
val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}")) val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
if (recentsJson.has(emojiShortname)) { if (recentsJson.has(emojiShortname)) {
val useCount = recentsJson.getInt(emojiShortname) val useCount = recentsJson.getInt(emojiShortname)
recentsJson.put(emojiShortname, useCount + 1) recentsJson.put(emojiShortname, useCount + 1)
} else { } else {
recentsJson.put(emojiShortname, 1) recentsJson.put(emojiShortname, 1)
} }
preferences.edit().putString(PREF_EMOJI_RECENTS, recentsJson.toString()).apply() preferences.edit().putString(PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
} }
internal suspend fun getCustomEmojisAsync(): List<Emoji> {
return withContext(CommonPool) {
db.emojiDao().loadAllCustomEmojis().also {
this.customEmojis = it
}
}
}
internal fun getCustomEmojis(): List<Emoji> = customEmojis
/** /**
* Get all recently used emojis ordered by usage count. * Get all recently used emojis ordered by usage count.
* *
* @return All recent emojis ordered by usage. * @return All recent emojis ordered by usage.
*/ */
internal fun getRecents(): List<Emoji> { internal suspend fun getRecents(): List<Emoji> = withContext(CommonPool) {
val list = mutableListOf<Emoji>() val list = mutableListOf<Emoji>()
val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}")) val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
for (shortname in recentsJson.keys()) {
val emoji = getEmojiByShortname(shortname) val allEmojis = db.emojiDao().loadAllEmojis()
emoji?.let { val len = recentsJson.length()
val recentShortnames = recentsJson.keys()
for (i in 0 until len) {
val shortname = recentShortnames.next()
allEmojis.firstOrNull {
if (it.shortname == shortname) {
if (it.isCustom()) {
return@firstOrNull getCurrentServerUrl()?.let { url ->
it.url?.startsWith(url)
} ?: false
}
return@firstOrNull true
}
false
}?.let {
val useCount = recentsJson.getInt(it.shortname) val useCount = recentsJson.getInt(it.shortname)
list.add(it.copy(count = useCount)) list.add(it.copy(count = useCount))
} }
} }
list.sortWith(Comparator { o1, o2 -> list.sortWith(Comparator { o1, o2 ->
o2.count - o1.count o2.count - o1.count
}) })
return list
return@withContext list
} }
/** /**
* Replace shortnames to unicode characters. * Replace shortnames to unicode characters.
*/ */
fun shortnameToUnicode(input: CharSequence, removeIfUnsupported: Boolean): String { fun shortnameToUnicode(input: CharSequence): String {
val matcher = SHORTNAME_PATTERN.matcher(input) val matcher = SHORTNAME_PATTERN.matcher(input)
var result: String = input.toString() var result: String = input.toString()
...@@ -163,7 +252,7 @@ object EmojiRepository { ...@@ -163,7 +252,7 @@ object EmojiRepository {
return result return result
} }
private fun loadEmojis(stream: InputStream): List<Emoji> { private fun loadEmojis(stream: InputStream): MutableList<Emoji> {
val emojisJSON = JSONArray(inputStreamToString(stream)) val emojisJSON = JSONArray(inputStreamToString(stream))
val emojis = ArrayList<Emoji>(emojisJSON.length()); val emojis = ArrayList<Emoji>(emojisJSON.length());
for (i in 0 until emojisJSON.length()) { for (i in 0 until emojisJSON.length()) {
......
...@@ -7,12 +7,17 @@ import chat.rocket.android.emoji.EmojiRepository ...@@ -7,12 +7,17 @@ import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.EmojiTypefaceSpan import chat.rocket.android.emoji.EmojiTypefaceSpan
import chat.rocket.android.emoji.R import chat.rocket.android.emoji.R
internal enum class EmojiCategory { enum class EmojiCategory {
RECENTS { RECENTS {
override fun resourceIcon() = R.drawable.ic_emoji_recents override fun resourceIcon() = R.drawable.ic_emoji_recents
override fun textIcon() = getTextIconFor("\uD83D\uDD58") override fun textIcon() = getTextIconFor("\uD83D\uDD58")
}, },
CUSTOM {
override fun resourceIcon() = R.drawable.ic_emoji_custom
override fun textIcon() = getTextIconFor("\uD83D\uDD58")
},
PEOPLE() { PEOPLE() {
override fun resourceIcon() = R.drawable.ic_emoji_people override fun resourceIcon() = R.drawable.ic_emoji_people
...@@ -65,4 +70,4 @@ internal enum class EmojiCategory { ...@@ -65,4 +70,4 @@ internal enum class EmojiCategory {
setSpan(span, 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) 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 ...@@ -14,7 +14,9 @@ import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiRepository import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.R 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_category_layout.view.*
import kotlinx.android.synthetic.main.emoji_image_row_item.view.*
import kotlinx.android.synthetic.main.emoji_row_item.view.* import kotlinx.android.synthetic.main.emoji_row_item.view.*
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.android.UI
...@@ -43,22 +45,33 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -43,22 +45,33 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
container.addView(view) container.addView(view)
launch(UI) { launch(UI) {
val currentServerUrl = EmojiRepository.getCurrentServerUrl()
val emojis = if (category != EmojiCategory.RECENTS) { 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 { } else {
sequenceOf(*EmojiRepository.getRecents().toTypedArray()) sequenceOf(*EmojiRepository.getRecents().toTypedArray())
} }
val recentEmojiSize = EmojiRepository.getRecents().size val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0 text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
if (adapters[category] == null) { if (adapters[category] == null) {
val adapter = EmojiAdapter(listener = listener) val adapter = EmojiAdapter(listener = listener)
emoji_recycler_view.adapter = adapter emoji_recycler_view.adapter = adapter
adapters[category] = adapter adapters[category] = adapter
adapter.addEmojisFromSequence(emojis) adapter.addEmojisFromSequence(emojis)
} }
adapters[category]!!.setFitzpatrick(fitzpatrick) adapters[category]!!.setFitzpatrick(fitzpatrick)
} }
} }
return view return view
} }
...@@ -88,20 +101,24 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -88,20 +101,24 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
private val listener: EmojiKeyboardListener private val listener: EmojiKeyboardListener
) : RecyclerView.Adapter<EmojiRowViewHolder>() { ) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private val CUSTOM = 1
private val NORMAL = 2
private val allEmojis = mutableListOf<Emoji>()
private val emojis = mutableListOf<Emoji>() private val emojis = mutableListOf<Emoji>()
fun addEmojis(emojis: List<Emoji>) { override fun getItemViewType(position: Int): Int {
this.emojis.clear() return if (emojis[position].isCustom()) CUSTOM else NORMAL
this.emojis.addAll(emojis)
notifyDataSetChanged()
} }
suspend fun addEmojisFromSequence(emojiSequence: Sequence<Emoji>) { suspend fun addEmojisFromSequence(emojiSequence: Sequence<Emoji>) {
withContext(CommonPool) { withContext(CommonPool) {
emojiSequence.forEachIndexed { index, emoji -> emojiSequence.forEachIndexed { index, emoji ->
withContext(UI) { withContext(UI) {
emojis.add(emoji) allEmojis.add(emoji)
notifyItemInserted(index) if (emoji.isDefault) {
emojis.add(emoji)
notifyItemInserted(emojis.size - 1)
}
} }
} }
} }
...@@ -115,12 +132,26 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -115,12 +132,26 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) { override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
val emoji = emojis[position] val emoji = emojis[position]
holder.bind( 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 { 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) return EmojiRowViewHolder(view, listener)
} }
...@@ -134,16 +165,26 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -134,16 +165,26 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
fun bind(emoji: Emoji) { fun bind(emoji: Emoji) {
with(itemView) { with(itemView) {
val parsedUnicode = unicodeCache[emoji.unicode] if (emoji.unicode.isNotEmpty()) {
emoji_view.setSpannableFactory(spannableFactory) // Handle simple emoji.
emoji_view.text = if (parsedUnicode == null) { val parsedUnicode = unicodeCache[emoji.unicode]
EmojiParser.parse(emoji.unicode, spannableFactory).let { emoji_view.setSpannableFactory(spannableFactory)
unicodeCache[emoji.unicode] = it emoji_view.text = if (parsedUnicode == null) {
it EmojiParser.parse(itemView.context, emoji.unicode, spannableFactory).let {
unicodeCache[emoji.unicode] = it
it
}
} else {
parsedUnicode
} }
} else { } else {
parsedUnicode // Handle custom emoji.
GlideApp.with(context)
.load(emoji.url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(emoji_image_view)
} }
itemView.setOnClickListener { itemView.setOnClickListener {
listener.onEmojiAdded(emoji) listener.onEmojiAdded(emoji)
} }
...@@ -155,4 +196,4 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -155,4 +196,4 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
private val unicodeCache = mutableMapOf<CharSequence, CharSequence>() 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 @@ ...@@ -6,6 +6,7 @@
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:gravity="center"
android:textColor="#000000" android:textColor="#000000"
android:textSize="26sp" android:textSize="26sp"
tools:text="😀" /> tools:text="😀" />
...@@ -5,5 +5,6 @@ ...@@ -5,5 +5,6 @@
<dimen name="supposed_keyboard_height">252dp</dimen> <dimen name="supposed_keyboard_height">252dp</dimen>
<dimen name="picker_popup_height">250dp</dimen> <dimen name="picker_popup_height">250dp</dimen>
<dimen name="picker_popup_width">300dp</dimen> <dimen name="picker_popup_width">300dp</dimen>
<dimen name="custom_emoji_large">32dp</dimen>
</resources> <dimen name="custom_emoji_small">22dp</dimen>
\ No newline at end of file </resources>
#Wed Aug 01 22:00:00 EDT 2018 #Mon Aug 06 11:30:07 BRT 2018
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME 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