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

Merge branch 'v3.0.0' into new/redesign-authentication-views

parents d85b30af 123d05d2
......@@ -16,8 +16,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 2042
versionName "2.6.0"
versionCode 2043
versionName "2.6.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
......@@ -93,6 +93,7 @@ dependencies {
implementation project(':draw')
implementation project(':util')
implementation project(':core')
implementation project(':suggestions')
implementation libraries.kotlin
implementation libraries.coroutines
......
......@@ -65,7 +65,9 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>(
val manager = FlexboxLayoutManager(context, FlexDirection.ROW)
recyclerView.layoutManager = manager
recyclerView.adapter = adapter
adapter.addReactions(it.reactions.filterNot { it.unicode.startsWith(":") })
adapter.addReactions(it.reactions.filterNot { reactionUiModel ->
reactionUiModel.unicode.startsWith(":") && reactionUiModel.url.isNullOrEmpty()
})
}
}
}
......
......@@ -7,9 +7,9 @@ import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter.CommandSuggestionsViewHolder
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.ui.BaseSuggestionViewHolder
import chat.rocket.android.suggestions.ui.SuggestionsAdapter
class CommandSuggestionsAdapter : SuggestionsAdapter<CommandSuggestionsViewHolder>(token = "/",
constraint = CONSTRAINT_BOUND_TO_START, threshold = RESULT_COUNT_UNLIMITED) {
......
package chat.rocket.android.chatroom.adapter
import android.annotation.SuppressLint
import android.text.SpannableStringBuilder
import android.text.style.ImageSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.EmojiSuggestionsAdapter.EmojiSuggestionViewHolder
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.internal.isCustom
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.strategy.trie.TrieCompletionStrategy
import chat.rocket.android.suggestions.ui.BaseSuggestionViewHolder
import chat.rocket.android.suggestions.ui.SuggestionsAdapter
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.suggestion_emoji_item.view.*
class EmojiSuggestionsAdapter : SuggestionsAdapter<EmojiSuggestionViewHolder>(
token = ":",
completionStrategy = TrieCompletionStrategy()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiSuggestionViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.suggestion_emoji_item, parent,false)
return EmojiSuggestionViewHolder(view)
}
class EmojiSuggestionViewHolder(view: View) : BaseSuggestionViewHolder(view) {
@SuppressLint("SetTextI18n")
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as EmojiSuggestionUiModel
with(itemView) {
text_emoji_shortname.text = ":${item.text}"
if (item.emoji.isCustom()) {
view_flipper_emoji.displayedChild = 1
val sp = SpannableStringBuilder().append(item.emoji.shortname)
Glide.with(context).asDrawable().load(item.emoji.url).into(image_emoji)
} else {
text_emoji.text = EmojiParser.parse(context, item.emoji.unicode)
view_flipper_emoji.displayedChild = 0
}
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
package chat.rocket.android.chatroom.adapter
import android.animation.ValueAnimator
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.animation.LinearInterpolator
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.MessageAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message_attachment.view.*
class MessageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageAttachmentUiModel>(itemView, listener, reactionListener) {
private var expanded = true
init {
with(itemView) {
setupActionMenu(attachment_container)
......@@ -21,9 +27,77 @@ class MessageAttachmentViewHolder(
override fun bindViews(data: MessageAttachmentUiModel) {
with(itemView) {
val collapsedHeight = context.resources.getDimensionPixelSize(R.dimen.quote_collapsed_height)
val viewMore = context.getString(R.string.msg_view_more)
val viewLess = context.getString(R.string.msg_view_less)
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
text_view_more.text = viewLess
text_content.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int,
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
val textMeasuredHeight = bottom - top
if (collapsedHeight >= textMeasuredHeight) {
text_view_more.isVisible = false
text_content.removeOnLayoutChangeListener(this)
return
}
text_view_more.isVisible = true
val expandAnimation = ValueAnimator
.ofInt(collapsedHeight, textMeasuredHeight)
.setDuration(300)
expandAnimation.interpolator = LinearInterpolator()
val collapseAnimation = ValueAnimator
.ofInt(textMeasuredHeight, collapsedHeight)
.setDuration(300)
collapseAnimation.interpolator = LinearInterpolator()
val lp = text_content.layoutParams
expandAnimation.addUpdateListener {
val value = it.animatedValue as Int
lp.height = value
text_content.layoutParams = lp
expanded = if (value == textMeasuredHeight) {
text_view_more.text = viewLess
true
} else {
text_view_more.text = viewMore
false
}
}
collapseAnimation.addUpdateListener {
val value = it.animatedValue as Int
lp.height = value
text_content.layoutParams = lp
expanded = if (value == textMeasuredHeight) {
text_view_more.text = viewLess
true
} else {
text_view_more.text = viewMore
false
}
}
text_view_more.setOnClickListener {
if (expandAnimation.isRunning) return@setOnClickListener
if (expanded) {
collapseAnimation.start()
} else {
expandAnimation.start()
}
}
text_content.removeOnLayoutChangeListener(this)
}
})
}
}
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.ReactionUiModel
......@@ -13,15 +14,13 @@ import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiKeyboardListener
import chat.rocket.android.emoji.EmojiPickerPopup
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.infrastructure.LocalRepository
import kotlinx.android.synthetic.main.item_reaction.view.*
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val REACTION_VIEW_TYPE = 0
private const val ADD_REACTION_VIEW_TYPE = 1
}
private val reactions = CopyOnWriteArrayList<ReactionUiModel>()
var listener: EmojiReactionListener? = null
......@@ -74,9 +73,11 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
fun contains(reactionShortname: String) =
reactions.firstOrNull { it.shortname == reactionShortname } != null
class SingleReactionViewHolder(view: View,
private val listener: EmojiReactionListener?)
: RecyclerView.ViewHolder(view), View.OnClickListener {
class SingleReactionViewHolder(
view: View,
private val listener: EmojiReactionListener?
) : RecyclerView.ViewHolder(view), View.OnClickListener {
@Inject
lateinit var localRepository: LocalRepository
@Volatile
......@@ -95,23 +96,33 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
clickHandled = false
this.reaction = reaction
with(itemView) {
val emojiTextView = findViewById<TextView>(R.id.text_emoji)
val countTextView = findViewById<TextView>(R.id.text_count)
emojiTextView.text = reaction.unicode
countTextView.text = reaction.count.toString()
if (reaction.url.isNullOrEmpty()) {
text_emoji.text = reaction.unicode
view_flipper_reaction.displayedChild = 0
} else {
view_flipper_reaction.displayedChild = 1
val glideRequest = if (reaction.url!!.endsWith("gif", true)) {
GlideApp.with(context).asGif()
} else {
GlideApp.with(context).asBitmap()
}
glideRequest.load(reaction.url).into(image_emoji)
}
text_count.text = reaction.count.toString()
val myself = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
if (reaction.usernames.contains(myself)) {
val context = itemView.context
val resources = context.resources
countTextView.setTextColor(resources.getColor(R.color.colorAccent))
text_count.setTextColor(ContextCompat.getColor(context, R.color.colorAccent))
}
emojiTextView.setOnClickListener(this@SingleReactionViewHolder)
countTextView.setOnClickListener(this@SingleReactionViewHolder)
view_flipper_reaction.setOnClickListener(this@SingleReactionViewHolder)
text_count.setOnClickListener(this@SingleReactionViewHolder)
}
}
override fun onClick(v: View?) {
override fun onClick(v: View) {
synchronized(this) {
if (!clickHandled) {
clickHandled = true
......@@ -121,8 +132,11 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
}
}
class AddReactionViewHolder(view: View,
private val listener: EmojiReactionListener?) : RecyclerView.ViewHolder(view) {
class AddReactionViewHolder(
view: View,
private val listener: EmojiReactionListener?
) : RecyclerView.ViewHolder(view) {
fun bind(messageId: String) {
itemView as ImageView
itemView.setOnClickListener {
......@@ -136,4 +150,9 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
}
}
}
}
\ No newline at end of file
companion object {
private const val REACTION_VIEW_TYPE = 0
private const val ADD_REACTION_VIEW_TYPE = 1
}
}
......@@ -13,9 +13,7 @@ class MessageReplyViewHolder(
) : BaseViewHolder<MessageReplyUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(itemView)
}
setupActionMenu(itemView)
}
override fun bindViews(data: MessageReplyUiModel) {
......
......@@ -11,9 +11,9 @@ import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter.PeopleSuggestionViewHolder
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.ui.BaseSuggestionViewHolder
import chat.rocket.android.suggestions.ui.SuggestionsAdapter
import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter(context: Context) : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
......
......@@ -7,9 +7,9 @@ import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter.RoomSuggestionsViewHolder
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.ui.BaseSuggestionViewHolder
import chat.rocket.android.suggestions.ui.SuggestionsAdapter
class RoomSuggestionsAdapter : SuggestionsAdapter<RoomSuggestionsViewHolder>("#") {
......
......@@ -13,10 +13,12 @@ import chat.rocket.android.chatroom.uimodel.RoomUiModel
import chat.rocket.android.chatroom.uimodel.UiModelMapper
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
......@@ -34,7 +36,6 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extension.compressImageAndGetByteArray
import chat.rocket.android.util.extension.compressImageAndGetInputStream
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.retryIO
......@@ -80,9 +81,7 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.threeten.bp.Instant
import timber.log.Timber
import java.io.InputStream
import java.util.*
import java.util.zip.DeflaterInputStream
import javax.inject.Inject
class ChatRoomPresenter @Inject constructor(
......@@ -180,10 +179,10 @@ class ChatRoomPresenter @Inject constructor(
val localMessages = messagesRepository.getByRoomId(chatRoomId)
val oldMessages = mapper.map(
localMessages, RoomUiModel(
roles = chatRoles,
// FIXME: Why are we fixing isRoom attribute to true here?
isBroadcast = chatIsBroadcast, isRoom = true
)
roles = chatRoles,
// FIXME: Why are we fixing isRoom attribute to true here?
isBroadcast = chatIsBroadcast, isRoom = true
)
)
val lastSyncDate = messagesRepository.getLastSyncDate(chatRoomId)
if (oldMessages.isNotEmpty() && lastSyncDate != null) {
......@@ -419,11 +418,12 @@ class ChatRoomPresenter @Inject constructor(
}
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error uploading file")
when (ex) {
is RocketChatException -> view.showMessage(ex)
else -> view.showGenericErrorMessage()
} catch (ex: RocketChatException) {
Timber.d(ex)
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
......@@ -597,9 +597,9 @@ class ChatRoomPresenter @Inject constructor(
replyMarkdown = "[ ]($currentServer/$chatRoomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map(
message, RoomUiModel(
roles = chatRoles,
isBroadcast = chatIsBroadcast
)
roles = chatRoles,
isBroadcast = chatIsBroadcast
)
).last().preview?.message ?: ""
)
}
......@@ -868,7 +868,7 @@ class ChatRoomPresenter @Inject constructor(
}
it.chatRoom.name == name || it.chatRoom.fullname == name
}.map {
with (it.chatRoom) {
with(it.chatRoom) {
ChatRoom(
id = id,
subscriptionId = subscriptionId,
......@@ -1008,6 +1008,20 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun loadEmojis() {
launchUI(strategy) {
val emojiSuggestionUiModels = EmojiRepository.getAll().map {
EmojiSuggestionUiModel(
text = it.shortname.replaceFirst(":", ""),
pinned = false,
emoji = it,
searchList = listOf(it.shortname)
)
}
view.populateEmojiSuggestions(emojis = emojiSuggestionUiModels)
}
}
fun runCommand(text: String, roomId: String) {
launchUI(strategy) {
try {
......@@ -1103,8 +1117,8 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(
streamedMessage, RoomUiModel(
roles = chatRoles, isBroadcast = chatIsBroadcast, isRoom = true
)
roles = chatRoles, isBroadcast = chatIsBroadcast, isRoom = true
)
)
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
......@@ -1126,4 +1140,33 @@ class ChatRoomPresenter @Inject constructor(
navigator.toMessageInformation(messageId = messageId)
}
}
}
\ No newline at end of file
/**
* Save unfinished message, when user left chat room without sending a message. It also clears
* saved message from local repository when unfinishedMessage is blank.
*
* @param chatRoomId Chat room Id.
* @param unfinishedMessage The unfinished message to save.
*/
fun saveUnfinishedMessage(chatRoomId: String, unfinishedMessage: String) {
val key = "${currentServer}_${LocalRepository.UNFINISHED_MSG_KEY}$chatRoomId"
if (unfinishedMessage.isNotBlank()) {
localRepository.save(key, unfinishedMessage)
} else {
localRepository.clear(key)
}
}
/**
* Get unfinished message from local repository, when user left chat room without
* sending a message and now the user is back.
*
* @param chatRoomId Chat room Id.
*
* @return Returns the unfinished message.
*/
fun getUnfinishedMessage(chatRoomId: String): String {
val key = "${currentServer}_${LocalRepository.UNFINISHED_MSG_KEY}$chatRoomId"
return localRepository.get(key) ?: ""
}
}
......@@ -3,6 +3,7 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
......@@ -127,6 +128,9 @@ interface ChatRoomView : LoadingView, MessageView {
fun populatePeopleSuggestions(members: List<PeopleSuggestionUiModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionUiModel>)
fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>)
/**
* This user has joined the chat callback.
*
......
......@@ -6,13 +6,11 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.os.SystemClock
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ImageSpan
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
......@@ -37,6 +35,7 @@ import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.EmojiSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
......@@ -47,6 +46,7 @@ import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.draw.main.ui.DRAWING_BYTE_ARRAY_EXTRA_DATA
import chat.rocket.android.draw.main.ui.DrawingActivity
......@@ -63,7 +63,6 @@ import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.circularRevealOrUnreveal
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
......@@ -76,8 +75,6 @@ import chat.rocket.android.util.extensions.ui
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import com.bumptech.glide.load.resource.gif.GifDrawable
import dagger.android.support.AndroidSupportInjection
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
......@@ -86,8 +83,6 @@ import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
......@@ -258,6 +253,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
recycler_view.removeOnLayoutChangeListener(layoutChangeListener)
presenter.disconnect()
presenter.saveUnfinishedMessage(chatRoomId, text_message.text.toString())
handler.removeCallbacksAndMessages(null)
unsubscribeComposeTextMessage()
......@@ -334,7 +330,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
val currentDayMarkerText = msgModel.currentDayMarkerText
val previousDayMarkerText = prevMsgModel.currentDayMarkerText
println("$previousDayMarkerText then $currentDayMarkerText")
if (previousDayMarkerText != currentDayMarkerText) {
prevMsgModel.showDayMarker = true
}
......@@ -477,15 +472,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
ui {
when (usernameList.size) {
1 -> text_typing_status.text =
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_is_typing))
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_is_typing))
2 -> text_typing_status.text =
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_and))
.bold { append(usernameList[1]) }
.append(getString(R.string.msg_are_typing))
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_and))
.bold { append(usernameList[1]) }
.append(getString(R.string.msg_are_typing))
else -> text_typing_status.text = getString(R.string.msg_several_users_are_typing)
}
......@@ -563,11 +558,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun showReplyingAction(
username: String,
replyMarkdown: String,
quotedMessage: String
) {
override fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String) {
ui {
citation = replyMarkdown
actionSnackbar.title = username
......@@ -617,6 +608,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>) {
ui {
suggestions_view.addItems(":", emojis)
}
}
override fun copyToClipboard(message: String) {
ui {
val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
......@@ -776,10 +773,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
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(
......@@ -797,6 +790,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
)
subscribeComposeTextMessage()
getUnfinishedMessage()
emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener {
......@@ -843,16 +837,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}, 400)
}
button_add_reaction.setOnClickListener { view ->
button_add_reaction.setOnClickListener { _ ->
openEmojiKeyboardPopup()
}
button_drawing.setOnClickListener {
activity?.let {
if (!ImageHelper.canWriteToExternalStorage(it)) {
ImageHelper.checkWritingPermission(it)
activity?.let { fragmentActivity ->
if (!ImageHelper.canWriteToExternalStorage(fragmentActivity)) {
ImageHelper.checkWritingPermission(fragmentActivity)
} else {
val intent = Intent(it, DrawingActivity::class.java)
val intent = Intent(fragmentActivity, DrawingActivity::class.java)
startActivityForResult(intent, REQUEST_CODE_FOR_DRAW)
}
}
......@@ -864,12 +858,26 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun getUnfinishedMessage() {
val unfinishedMessage = presenter.getUnfinishedMessage(chatRoomId)
if (unfinishedMessage.isNotBlank()) {
text_message.setText(unfinishedMessage)
val orientation = resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
KeyboardHelper.showSoftKeyboard(text_message)
} else {
//TODO show keyboard in full screen mode when landscape orientation
}
}
}
private fun setupSuggestionsView() {
suggestions_view.anchorTo(text_message)
.setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height))
.addTokenAdapter(PeopleSuggestionsAdapter(context!!))
.addTokenAdapter(CommandSuggestionsAdapter())
.addTokenAdapter(RoomSuggestionsAdapter())
.addTokenAdapter(EmojiSuggestionsAdapter())
.addSuggestionProviderAction("@") { query ->
if (query.isNotEmpty()) {
presenter.spotlight(query, PEOPLE, true)
......@@ -880,10 +888,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
presenter.loadChatRooms()
}
}
.addSuggestionProviderAction("/") { _ ->
.addSuggestionProviderAction("/") {
presenter.loadCommands()
}
.addSuggestionProviderAction(":") {
presenter.loadEmojis()
}
presenter.loadEmojis()
presenter.loadCommands()
}
......
package chat.rocket.android.chatroom.ui
import org.commonmark.ext.gfm.strikethrough.Strikethrough
import org.commonmark.node.Node
import org.commonmark.node.Text
import org.commonmark.parser.delimiter.DelimiterProcessor
import org.commonmark.parser.delimiter.DelimiterRun
class StrikethroughDelimiterProcessor : DelimiterProcessor {
override fun getOpeningCharacter(): Char {
return '~'
}
override fun getClosingCharacter(): Char {
return '~'
}
override fun getMinLength(): Int {
return 1
}
override fun getDelimiterUse(opener: DelimiterRun, closer: DelimiterRun): Int {
return if (opener.length() >= 2 && closer.length() >= 2) {
// Use exactly two delimiters even if we have more, and don't care about internal openers/closers.
2
} else {
1
}
}
override fun process(opener: Text, closer: Text, delimiterCount: Int) {
// Wrap nodes between delimiters in strikethrough.
val strikethrough = Strikethrough()
var tmp: Node? = opener.next
while (tmp != null && tmp !== closer) {
val next = tmp.next
strikethrough.appendChild(tmp)
tmp = next
}
opener.insertAfter(strikethrough)
}
}
......@@ -5,5 +5,6 @@ data class ReactionUiModel(
val shortname: String,
val unicode: CharSequence,
val count: Int,
val usernames: List<String> = emptyList()
val usernames: List<String> = emptyList(),
var url: String? = null
)
\ No newline at end of file
......@@ -18,6 +18,7 @@ import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UserHelper
......@@ -504,15 +505,18 @@ class UiModelMapper @Inject constructor(
private fun getReactions(message: Message): List<ReactionUiModel> {
val reactions = message.reactions?.let {
val list = mutableListOf<ReactionUiModel>()
val customEmojis = EmojiRepository.getCustomEmojis()
it.getShortNames().forEach { shortname ->
val usernames = it.getUsernames(shortname) ?: emptyList()
val count = usernames.size
val custom = customEmojis.firstOrNull { emoji -> emoji.shortname == shortname }
list.add(
ReactionUiModel(messageId = message.id,
shortname = shortname,
unicode = EmojiParser.parse(context, shortname),
count = count,
usernames = usernames)
usernames = usernames,
url = custom?.url)
)
}
list
......
package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.suggestions.model.SuggestionModel
class ChatRoomSuggestionUiModel(text: String,
val fullName: String,
......
package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.suggestions.model.SuggestionModel
class CommandSuggestionUiModel(text: String,
val description: String,
......
package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.common.model.UserStatus
class EmojiSuggestionUiModel(
text: String,
pinned: Boolean = false,
val emoji: Emoji,
searchList: List<String>
) : SuggestionModel(text, searchList, pinned) {
override fun toString(): String {
return "EmojiSuggestionUiModel(text='$text', searchList='$searchList', pinned=$pinned)"
}
}
package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.common.model.UserStatus
class PeopleSuggestionUiModel(val imageUri: String?,
......
......@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
......@@ -50,4 +51,4 @@ object KeyboardHelper {
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.SHOW_IMPLICIT)
}
}
}
\ No newline at end of file
}
......@@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.Spannable
import android.text.Spanned
import android.text.style.ClickableSpan
import android.text.style.ImageSpan
......@@ -13,6 +14,7 @@ import android.util.Patterns
import android.view.View
import androidx.core.content.res.ResourcesCompat
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.StrikethroughDelimiterProcessor
import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.EmojiTypefaceSpan
......@@ -21,16 +23,23 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message
import org.commonmark.Extension
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import ru.noties.markwon.Markwon
import org.commonmark.parser.Parser
import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import ru.noties.markwon.tasklist.TaskListExtension
import java.util.*
import javax.inject.Inject
class MessageParser @Inject constructor(
......@@ -39,8 +48,6 @@ class MessageParser @Inject constructor(
private val settings: PublicSettings
) {
private val parser = Markwon.createParser()
/**
* Render markdown and other rules on message to rich text with spans.
*
......@@ -52,6 +59,16 @@ class MessageParser @Inject constructor(
fun render(message: Message, selfUsername: String? = null): CharSequence {
var text: String = message.message
val mentions = mutableListOf<String>()
val parser = Parser.Builder()
.extensions(Arrays.asList<Extension>(
StrikethroughExtension.create(),
TablesExtension.create(),
TaskListExtension.create()
))
.customDelimiterProcessor(StrikethroughDelimiterProcessor())
.build()
message.mentions?.forEach {
val mention = getMention(it)
mentions.add(mention)
......@@ -59,12 +76,17 @@ class MessageParser @Inject constructor(
text = text.replace("@${it.username}", mention)
}
}
val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text)
val parentNode = parser.parse(toLenientMarkdown(content))
val parentNode = parser.parse(content)
parentNode.accept(EmphasisVisitor())
parentNode.accept(StrongEmphasisVisitor())
parentNode.accept(MarkdownVisitor(configuration, builder))
parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(context, configuration, builder))
message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername))
}
......@@ -72,13 +94,6 @@ class MessageParser @Inject constructor(
return builder.text()
}
// Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
private fun toLenientMarkdown(text: String): String {
return text.trim().replace("\\*(.+)\\*".toRegex()) { "**${it.groupValues[1].trim()}**" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
}
private fun getMention(user: SimpleUser): String {
return if (settings.useRealName()) {
user.name ?: "@${user.username}"
......@@ -87,6 +102,31 @@ class MessageParser @Inject constructor(
}
}
class EmphasisVisitor : AbstractVisitor() {
override fun visit(emphasis: Emphasis) {
if (emphasis.openingDelimiter == "*" && emphasis.firstChild != null) {
val child = emphasis.firstChild
val strongEmphasis = StrongEmphasis()
strongEmphasis.appendChild(child)
emphasis.insertBefore(strongEmphasis)
emphasis.unlink()
}
}
}
class StrongEmphasisVisitor : AbstractVisitor() {
override fun visit(strongEmphasis: StrongEmphasis) {
if (strongEmphasis.openingDelimiter == "__" && strongEmphasis.firstChild != null) {
val child = strongEmphasis.firstChild
val emphasis = Emphasis()
emphasis.appendChild(child)
strongEmphasis.insertBefore(emphasis)
strongEmphasis.unlink()
}
}
}
class MentionVisitor(
context: Context,
private val builder: SpannableBuilder,
......@@ -98,28 +138,28 @@ class MessageParser @Inject constructor(
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.colorWhite, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
private val padding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val radius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
override fun visit(t: Text) {
val text = t.literal
override fun visit(document: Document) {
val text = builder.text()
val mentionsList = mentions.toMutableList().also {
it.add("@all")
it.add("@here")
}.toList()
}.distinct()
mentionsList.forEach {
val mentionMe = it == currentUser || it == "@all" || it == "@here"
var offset = text.indexOf(it, 0, true)
var offset = text.indexOf(string = it, startIndex = 0, ignoreCase = false)
while (offset > -1) {
val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding,
val usernameSpan = MentionSpan(backgroundColor, textColor, radius, padding,
mentionMe)
// Add 1 to end offset to include the @.
val end = offset + it.length + 1
builder.setSpan(usernameSpan, offset, end, 0)
offset = text.indexOf("@$it", end, true)
val end = offset + it.length
builder.setSpan(usernameSpan, offset, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
offset = text.indexOf(string = "@$it", startIndex = end, ignoreCase = false)
}
}
}
......@@ -179,7 +219,7 @@ class MessageParser @Inject constructor(
}
private fun newLine() {
if (builder.length() > 0 && '\n' != builder.lastChar()) {
if (builder.isNotEmpty() && '\n' != builder.lastChar()) {
builder.append('\n')
}
}
......@@ -204,7 +244,6 @@ class MessageParser @Inject constructor(
consumed.add(link)
}
}
visitChildren(text)
}
}
......
......@@ -27,6 +27,7 @@ interface LocalRepository {
const val SETTINGS_KEY = "settings_"
const val PERMISSIONS_KEY = "permissions_"
const val USER_KEY = "user_"
const val UNFINISHED_MSG_KEY = "unfinished_msg_"
const val CURRENT_USERNAME_KEY = "username_"
const val LAST_CHATROOMS_REFRESH = "_chatrooms_refresh"
}
......
......@@ -5,5 +5,5 @@
android:width="24dp"
android:height="24dp" />
<solid android:color="#efeeee" />
<corners android:radius="4dp"/>
</shape>
\ No newline at end of file
<corners android:radius="4dp" />
</shape>
......@@ -74,7 +74,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView
<chat.rocket.android.suggestions.ui.SuggestionsView
android:id="@+id/suggestions_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
......
......@@ -5,13 +5,14 @@
android:id="@+id/attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
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"
android:paddingTop="@dimen/message_item_top_and_bottom_padding">
android:paddingTop="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding">
<View
android:id="@+id/quote_bar"
......@@ -19,7 +20,7 @@
android:layout_height="0dp"
android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_gray_bar"
app:layout_constraintBottom_toTopOf="@+id/recycler_view_reactions"
app:layout_constraintBottom_toTopOf="@+id/text_view_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
......@@ -28,11 +29,11 @@
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins"
app:layout_constraintStart_toEndOf="@+id/quote_bar"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="8dp" />
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_message_time"
......@@ -40,28 +41,39 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM"
app:layout_constraintBottom_toBottomOf="@+id/text_sender"
app:layout_constraintStart_toEndOf="@+id/text_sender"
app:layout_constraintTop_toTopOf="@+id/text_sender"
app:layout_constraintBottom_toBottomOf="@+id/text_sender"/>
tools:text="11:45 PM" />
<TextView
android:id="@+id/text_content"
style="@style/Message.Quote.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
app:layout_constraintBottom_toTopOf="@id/text_view_more"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_sender"
app:layout_constraintTop_toBottomOf="@+id/text_sender"
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!" />
<TextView
android:id="@+id/text_view_more"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/darkGray"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="@+id/text_content"
app:layout_constraintStart_toStartOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@+id/text_content"
tools:text="Visualizar mais" />
<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/text_content" />
app:layout_constraintTop_toBottomOf="@+id/text_view_more" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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"
android:layout_marginEnd="2dp"
android:layout_marginRight="2dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:descendantFocusability="beforeDescendants"
android:background="@drawable/rounded_background"
android:orientation="horizontal">
android:background="@drawable/rounded_background">
<TextView
android:id="@+id/text_emoji"
<ViewFlipper
android:id="@+id/view_flipper_reaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="4dp"
android:paddingStart="4dp"
android:textColor="#868585"
android:textSize="16sp"
tools:text=":)" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/text_count"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/text_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="4dp"
android:paddingLeft="4dp"
android:textColor="#868585"
android:textSize="16sp"
tools:text=":)" />
<ImageView
android:id="@+id/image_emoji"
android:layout_width="@dimen/custom_emoji_small"
android:layout_height="@dimen/custom_emoji_small"
android:layout_gravity="center"
tools:src="@tools:sample/avatars" />
</ViewFlipper>
<TextView
android:id="@+id/text_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="4dp"
android:paddingEnd="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingStart="4dp"
android:paddingLeft="4dp"
android:paddingTop="4dp"
android:paddingEnd="4dp"
android:paddingRight="4dp"
android:paddingBottom="4dp"
android:textColor="#868585"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/view_flipper_reaction"
app:layout_constraintTop_toTopOf="parent"
tools:text="12" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<?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="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="2dp"
android:layout_marginBottom="2dp"
android:background="@color/suggestion_background_color">
<ViewFlipper
android:id="@+id/view_flipper_emoji"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/text_emoji"
android:layout_width="24dp"
android:layout_height="24dp"
android:textColor="@android:color/black"
tools:text=":)" />
<ImageView
android:id="@+id/image_emoji"
android:layout_width="24dp"
android:layout_height="24dp"
tools:src="@tools:sample/avatars" />
</ViewFlipper>
<TextView
android:id="@+id/text_emoji_shortname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:maxLines="1"
android:textColor="@color/colorBlack"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/view_flipper_emoji"
app:layout_constraintTop_toTopOf="parent"
tools:text=":grinning:" />
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -157,6 +157,10 @@
<string name="msg_message_copied">Nachricht kopiert</string>
<string name="msg_delete_message">Lösche Nachricht</string>
<string name="msg_delete_description">Sind Sie sicher, dass Sie diese Nachricht löschen wollen?</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string>
<!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
......
......@@ -154,6 +154,10 @@
<string name="msg_member_already_added">Ya has seleccionado este usuario</string>
<string name="msg_member_not_found">Miembro no encontrado</string>
<string name="msg_channel_created_successfully">Canal creado con éxito</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string>
<!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
......
......@@ -147,6 +147,10 @@
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation -->
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation -->
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation -->
<!-- TODO - Add proper translation -->
<string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Privé</string>
......
......@@ -160,6 +160,10 @@
<string name="msg_member_already_added">आपने पहले से ही इस यूजर को चुन चुके है।</string>
<string name="msg_member_not_found">सदस्य नहीं मिला</string>
<string name="msg_channel_created_successfully">चैनल सफलतापूर्वक बनाया गया</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string>
<!-- Preferences messages -->
<string name="msg_analytics_tracking">एनालिटिक्स ट्रैकिंग</string>
......
......@@ -147,6 +147,10 @@
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation -->
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation -->
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation -->
<!-- TODO - Add proper translation -->
<string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string>
<!-- Create channel messages -->
<string name="msg_private_channel">プライベート</string>
......
......@@ -133,8 +133,8 @@
<string name="msg_upload_file">Subir arquivo</string>
<string name="msg_file_description">Descrição do arquivo</string>
<string name="msg_send">Enviar</string>
<string name="msg_delete_message">Delete Message</string> <!-- TODO Add translation -->
<string name="msg_delete_description">Are you sure you want to delete this message</string> <!-- TODO Add translation -->
<string name="msg_delete_message">Remove mensagem</string>
<string name="msg_delete_description">Tem certeza que quer apagar esta mensagem?</string>
<string name="msg_welcome_to_rocket_chat">Welcome to Rocket.Chat</string> <!-- TODO Add translation -->
<string name="msg_team_communication">Open Source Communication</string> <!-- TODO Add translation -->
<string name="msg_login_with_email">Login with <b>e-mail</b></string> <!-- TODO Add translation -->
......@@ -147,6 +147,8 @@
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation -->
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation -->
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation -->
<string name="msg_view_more">visualizar mais</string>
<string name="msg_view_less">visualizar menos</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Privado</string>
......
......@@ -145,6 +145,9 @@
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation -->
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation -->
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation -->
<string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Приватный</string>
......
......@@ -144,6 +144,10 @@
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation -->
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation -->
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation -->
<!-- TODO - Add proper translation -->
<string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Приватний</string>
......
......@@ -43,11 +43,9 @@
<dimen name="padding_mention">4dp</dimen>
<dimen name="radius_mention">6dp</dimen>
<!-- Autocomplete Popup -->
<dimen name="popup_max_height">150dp</dimen>
<dimen name="suggestions_box_max_height">250dp</dimen>
<dimen name="viewer_toolbar_padding">16dp</dimen>
<dimen name="viewer_toolbar_title">16sp</dimen>
<dimen name="quote_collapsed_height">32dp</dimen>
</resources>
\ No newline at end of file
......@@ -174,6 +174,8 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="msg_message_copied">Message copied</string>
<string name="msg_delete_message">Delete Message</string>
<string name="msg_delete_description">Are you sure you want to delete this message</string>
<string name="msg_view_more">view more</string>
<string name="msg_view_less">view less</string>
<!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string>
......
......@@ -10,7 +10,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-rc02'
classpath 'com.android.tools.build:gradle:3.2.0-rc03'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:4.0.2'
......
......@@ -48,7 +48,7 @@ ext {
kotshi : '1.0.4',
frescoImageViewer : '0.5.1',
markwon : '1.1.1',
markwon : '2.0.0',
aVLoadingIndicatorView: '2.1.3',
glide : '4.8.0',
......
......@@ -156,21 +156,24 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
private suspend fun setupViewPager() {
context.let {
val callback = when (it) {
val callback: EmojiKeyboardListener? = when (it) {
is EmojiKeyboardListener -> it
else -> {
val fragments = (it as AppCompatActivity).supportFragmentManager.fragments
if (fragments.size == 0 || !(fragments[0] is EmojiKeyboardListener)) {
throw IllegalStateException("activity/fragment should implement Listener interface")
// Since the app can arrive in an inconsistent state at this point, do not throw
// throw IllegalStateException("activity/fragment should implement Listener interface")
null
} else {
fragments[0] as EmojiKeyboardListener
}
fragments[0] as EmojiKeyboardListener
}
}
adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
callback.onEmojiAdded(emoji)
callback?.onEmojiAdded(emoji)
}
})
......
......@@ -146,7 +146,7 @@ object EmojiRepository {
*
* @return All emojis for all categories.
*/
internal suspend fun getAll(): List<Emoji> = withContext(CommonPool) {
suspend fun getAll(): List<Emoji> = withContext(CommonPool) {
return@withContext db.emojiDao().loadAllEmojis()
}
......@@ -210,7 +210,7 @@ object EmojiRepository {
}
}
internal fun getCustomEmojis(): List<Emoji> = customEmojis
fun getCustomEmojis(): List<Emoji> = customEmojis
/**
* Get all recently used emojis ordered by usage count.
......
include ':app', ':player', ':emoji', ':draw', ':util', ':core' //, ':wear'
\ No newline at end of file
include ':app', ':player', ':emoji', ':draw', ':util', ':core', ':suggestions' //, ':wear'
\ No newline at end of file
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
defaultConfig {
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation libraries.kotlin
implementation libraries.recyclerview
implementation libraries.appCompat
implementation libraries.material
}
androidExtensions {
experimental = true
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.rocket.android.suggestions" />
package chat.rocket.android.widget.autocompletion.model
package chat.rocket.android.suggestions.model
abstract class SuggestionModel(
val text: String, // This is the text key for searches, must be unique.
val searchList: List<String> = emptyList(), // Where to search for matches.
val pinned: Boolean = false /* If pinned item will have priority to show */
) {
abstract class SuggestionModel(val text: String, // This is the text key for searches, must be unique.
val searchList: List<String> = emptyList(), // Where to search for matches.
val pinned: Boolean = false /* If pinned item will have priority to show */) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SuggestionModel) return false
......@@ -15,4 +18,4 @@ abstract class SuggestionModel(val text: String, // This is the text key for sea
override fun hashCode(): Int {
return text.hashCode()
}
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.repository
package chat.rocket.android.suggestions.repository
interface LocalSuggestionProvider {
fun find(prefix: String)
......
package chat.rocket.android.widget.autocompletion.strategy
package chat.rocket.android.suggestions.strategy
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.suggestions.model.SuggestionModel
interface CompletionStrategy {
fun getItem(prefix: String, position: Int): SuggestionModel
......@@ -8,4 +8,4 @@ interface CompletionStrategy {
fun addAll(list: List<SuggestionModel>)
fun addPinned(list: List<SuggestionModel>)
fun size(): Int
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.strategy.regex
package chat.rocket.android.suggestions.strategy.regex
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.RESULT_COUNT_UNLIMITED
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.strategy.CompletionStrategy
import chat.rocket.android.suggestions.ui.SuggestionsAdapter.Companion.RESULT_COUNT_UNLIMITED
import java.util.concurrent.CopyOnWriteArrayList
internal class StringMatchingCompletionStrategy(private val threshold: Int = RESULT_COUNT_UNLIMITED) : CompletionStrategy {
......@@ -46,4 +46,4 @@ internal class StringMatchingCompletionStrategy(private val threshold: Int = RES
override fun size(): Int {
return list.size
}
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.strategy.trie
package chat.rocket.android.suggestions.strategy.trie
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.trie.data.Trie
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.strategy.CompletionStrategy
import chat.rocket.android.suggestions.strategy.trie.data.Trie
class TrieCompletionStrategy : CompletionStrategy {
private val items = mutableListOf<SuggestionModel>()
......@@ -18,7 +18,9 @@ class TrieCompletionStrategy : CompletionStrategy {
return item
}
override fun autocompleteItems(prefix: String) = trie.autocompleteItems(prefix)
override fun autocompleteItems(prefix: String): List<SuggestionModel> {
return trie.autocompleteItems(prefix)
}
override fun addAll(list: List<SuggestionModel>) {
items.addAll(list)
......@@ -28,8 +30,7 @@ class TrieCompletionStrategy : CompletionStrategy {
}
override fun addPinned(list: List<SuggestionModel>) {
}
override fun size() = items.size
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.strategy.trie.data
package chat.rocket.android.suggestions.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.suggestions.model.SuggestionModel
internal class Trie {
private val root = TrieNode(' ')
......@@ -34,10 +34,7 @@ internal class Trie {
val sanitizedWord = word.trim().toLowerCase()
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
return false
}
val child = current.getChild(ch) ?: return false
current = child
}
if (current.isLeaf) {
......@@ -63,8 +60,8 @@ internal class Trie {
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getItems()
return lastNode!!.getItems().take(5).toList()
}
fun getCount() = count
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.strategy.trie.data
package chat.rocket.android.suggestions.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.suggestions.model.SuggestionModel
import kotlin.coroutines.experimental.buildSequence
internal class TrieNode(
internal var data: Char,
internal var parent: TrieNode? = null,
internal var isLeaf: Boolean = false,
internal var item: SuggestionModel? = null
) {
internal class TrieNode(internal var data: Char,
internal var parent: TrieNode? = null,
internal var isLeaf: Boolean = false,
internal var item: SuggestionModel? = null) {
val children = hashMapOf<Char, TrieNode>()
fun getChild(c: Char): TrieNode? {
......@@ -28,20 +32,18 @@ internal class TrieNode(internal var data: Char,
return list
}
class X : SuggestionModel("")
fun getItems(): Sequence<SuggestionModel> = buildSequence {
fun getItems(): List<SuggestionModel> {
val list = arrayListOf<SuggestionModel>()
if (isLeaf) {
list.add(item!!)
yield(item!!)
}
children.forEach { node ->
node.value.let {
list.addAll(it.getItems())
yieldAll(it.getItems())
}
}
return list
}
override fun toString(): String = if (parent == null) "" else "${parent.toString()}$data"
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.ui
package chat.rocket.android.suggestions.ui
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.suggestions.model.SuggestionModel
abstract class BaseSuggestionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
abstract fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?)
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.ui
package chat.rocket.android.suggestions.ui
import android.content.Context
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.suggestions.R
internal class PopupRecyclerView : RecyclerView {
private var displayWidth: Int = 0
......@@ -38,4 +38,4 @@ internal class PopupRecyclerView : RecyclerView {
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l + 40, t, r - 40, b)
}
}
\ No newline at end of file
}
package chat.rocket.android.widget.autocompletion.ui
package chat.rocket.android.suggestions.ui
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.regex.StringMatchingCompletionStrategy
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.strategy.CompletionStrategy
import chat.rocket.android.suggestions.strategy.regex.StringMatchingCompletionStrategy
import java.lang.reflect.Type
import kotlin.properties.Delegates
abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
val token: String,
val constraint: Int = CONSTRAINT_UNBOUND,
threshold: Int = MAX_RESULT_COUNT) : RecyclerView.Adapter<VH>() {
companion object {
// Any number of results.
const val RESULT_COUNT_UNLIMITED = -1
// Trigger suggestions only if on the line start.
const val CONSTRAINT_BOUND_TO_START = 0
// Trigger suggestions from anywhere.
const val CONSTRAINT_UNBOUND = 1
// Maximum number of results to display by default.
private const val MAX_RESULT_COUNT = 5
}
completionStrategy: CompletionStrategy? = null,
threshold: Int = MAX_RESULT_COUNT
) : RecyclerView.Adapter<VH>() {
private var itemType: Type? = null
private var itemClickListener: ItemClickListener? = null
......@@ -30,12 +22,12 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
// Maximum number of results/suggestions to display.
private var resultsThreshold: Int = if (threshold > 0) threshold else RESULT_COUNT_UNLIMITED
// The strategy used for suggesting completions.
private val strategy: CompletionStrategy = StringMatchingCompletionStrategy(resultsThreshold)
private val strategy: CompletionStrategy = completionStrategy ?: StringMatchingCompletionStrategy(resultsThreshold)
// Current input term to look up for suggestions.
private var currentTerm: String by Delegates.observable("", { _, _, newTerm ->
private var currentTerm: String by Delegates.observable("") { _, _, newTerm ->
val items = strategy.autocompleteItems(newTerm)
notifyDataSetChanged()
})
}
init {
setHasStableIds(true)
......@@ -105,4 +97,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
interface ItemClickListener {
fun onClick(item: SuggestionModel)
}
}
\ No newline at end of file
companion object {
// Any number of results.
const val RESULT_COUNT_UNLIMITED = -1
// Trigger suggestions only if on the line start.
const val CONSTRAINT_BOUND_TO_START = 0
// Trigger suggestions from anywhere.
const val CONSTRAINT_UNBOUND = 1
// Maximum number of results to display by default.
private const val MAX_RESULT_COUNT = 5
}
}
package chat.rocket.android.widget.autocompletion.ui
package chat.rocket.android.suggestions.ui
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import androidx.transition.Slide
import androidx.transition.TransitionManager
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.transition.Slide
import android.transition.TransitionManager
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.FrameLayout
import chat.rocket.android.R
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.suggestions.R
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.android.suggestions.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicInteger
......@@ -103,7 +103,8 @@ class SuggestionsView : FrameLayout, TextWatcher {
val prefixEndIndex = this.editor?.get()?.selectionStart ?: NO_STATE_INDEX
if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionOffset.get()) return
val prefix = s.subSequence(completionOffset.get(), this.editor?.get()?.selectionStart ?: completionOffset.get()).toString()
val prefix = s.subSequence(completionOffset.get(), this.editor?.get()?.selectionStart
?: completionOffset.get()).toString()
recyclerView.adapter?.let {
it as SuggestionsAdapter
// we need to look up only after the '@'
......@@ -148,15 +149,14 @@ class SuggestionsView : FrameLayout, TextWatcher {
}
fun addTokenAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
adaptersByToken.getOrPut(adapter.token, { adapter })
adaptersByToken.getOrPut(adapter.token) { adapter }
return this
}
fun addItems(token: String, list: List<SuggestionModel>): SuggestionsView {
if (list.isNotEmpty()) {
val adapter = adapter(token)
localProvidersByToken.getOrPut(token, { hashMapOf() })
.put(adapter.term(), list)
localProvidersByToken.getOrPut(token) { hashMapOf() }.put(adapter.term(), list)
if (completionOffset.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand()
adapter.addItems(list)
}
......@@ -192,7 +192,8 @@ class SuggestionsView : FrameLayout, TextWatcher {
}
private fun adapter(token: String): SuggestionsAdapter<*> {
return adaptersByToken[token] ?: throw IllegalStateException("no adapter binds to token \"$token\"")
return adaptersByToken[token]
?: throw IllegalStateException("no adapter binds to token \"$token\"")
}
private fun cancelSuggestions(haltCompletion: Boolean) {
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="popup_max_height">150dp</dimen>
<dimen name="suggestions_box_max_height">250dp</dimen>
</resources>
\ No newline at end of file
<resources>
<string name="app_name">suggestions</string>
</resources>
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