Unverified Commit 5d7f2ed6 authored by Divyanshu Bhargava's avatar Divyanshu Bhargava Committed by GitHub

Merge pull request #56 from RocketChat/develop

merge
parents 7f75dc2a 123d05d2
...@@ -16,8 +16,8 @@ android { ...@@ -16,8 +16,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion versions.minSdk minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2042 versionCode 2043
versionName "2.6.0" versionName "2.6.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
......
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 package chat.rocket.android.chatroom.adapter
import android.animation.ValueAnimator
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View 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.chatroom.uimodel.MessageAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message_attachment.view.* import kotlinx.android.synthetic.main.item_message_attachment.view.*
class MessageAttachmentViewHolder( class MessageAttachmentViewHolder(
itemView: View, itemView: View,
listener: ActionsListener, listener: ActionsListener,
reactionListener: EmojiReactionListener? = null reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageAttachmentUiModel>(itemView, listener, reactionListener) { ) : BaseViewHolder<MessageAttachmentUiModel>(itemView, listener, reactionListener) {
private var expanded = true
init { init {
with(itemView) { with(itemView) {
setupActionMenu(attachment_container) setupActionMenu(attachment_container)
...@@ -21,9 +27,77 @@ class MessageAttachmentViewHolder( ...@@ -21,9 +27,77 @@ class MessageAttachmentViewHolder(
override fun bindViews(data: MessageAttachmentUiModel) { override fun bindViews(data: MessageAttachmentUiModel) {
with(itemView) { 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_message_time.text = data.time
text_sender.text = data.senderName text_sender.text = data.senderName
text_content.text = data.content 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
...@@ -13,9 +13,7 @@ class MessageReplyViewHolder( ...@@ -13,9 +13,7 @@ class MessageReplyViewHolder(
) : BaseViewHolder<MessageReplyUiModel>(itemView, listener, reactionListener) { ) : BaseViewHolder<MessageReplyUiModel>(itemView, listener, reactionListener) {
init { init {
with(itemView) { setupActionMenu(itemView)
setupActionMenu(itemView)
}
} }
override fun bindViews(data: MessageReplyUiModel) { override fun bindViews(data: MessageReplyUiModel) {
......
...@@ -13,10 +13,12 @@ import chat.rocket.android.chatroom.uimodel.RoomUiModel ...@@ -13,10 +13,12 @@ import chat.rocket.android.chatroom.uimodel.RoomUiModel
import chat.rocket.android.chatroom.uimodel.UiModelMapper import chat.rocket.android.chatroom.uimodel.UiModelMapper
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel 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.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.core.behaviours.showMessage import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.helper.MessageHelper import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.UserHelper import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
...@@ -34,7 +36,6 @@ import chat.rocket.android.server.domain.useRealName ...@@ -34,7 +36,6 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extension.compressImageAndGetByteArray 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.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
...@@ -80,9 +81,7 @@ import kotlinx.coroutines.experimental.launch ...@@ -80,9 +81,7 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import org.threeten.bp.Instant import org.threeten.bp.Instant
import timber.log.Timber import timber.log.Timber
import java.io.InputStream
import java.util.* import java.util.*
import java.util.zip.DeflaterInputStream
import javax.inject.Inject import javax.inject.Inject
class ChatRoomPresenter @Inject constructor( class ChatRoomPresenter @Inject constructor(
...@@ -180,10 +179,10 @@ class ChatRoomPresenter @Inject constructor( ...@@ -180,10 +179,10 @@ class ChatRoomPresenter @Inject constructor(
val localMessages = messagesRepository.getByRoomId(chatRoomId) val localMessages = messagesRepository.getByRoomId(chatRoomId)
val oldMessages = mapper.map( val oldMessages = mapper.map(
localMessages, RoomUiModel( localMessages, RoomUiModel(
roles = chatRoles, roles = chatRoles,
// FIXME: Why are we fixing isRoom attribute to true here? // FIXME: Why are we fixing isRoom attribute to true here?
isBroadcast = chatIsBroadcast, isRoom = true isBroadcast = chatIsBroadcast, isRoom = true
) )
) )
val lastSyncDate = messagesRepository.getLastSyncDate(chatRoomId) val lastSyncDate = messagesRepository.getLastSyncDate(chatRoomId)
if (oldMessages.isNotEmpty() && lastSyncDate != null) { if (oldMessages.isNotEmpty() && lastSyncDate != null) {
...@@ -419,11 +418,12 @@ class ChatRoomPresenter @Inject constructor( ...@@ -419,11 +418,12 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
} }
} catch (ex: Exception) { } catch (ex: RocketChatException) {
Timber.d(ex, "Error uploading file") Timber.d(ex)
when (ex) { ex.message?.let {
is RocketChatException -> view.showMessage(ex) view.showMessage(it)
else -> view.showGenericErrorMessage() }.ifNull {
view.showGenericErrorMessage()
} }
} finally { } finally {
view.hideLoading() view.hideLoading()
...@@ -597,9 +597,9 @@ class ChatRoomPresenter @Inject constructor( ...@@ -597,9 +597,9 @@ class ChatRoomPresenter @Inject constructor(
replyMarkdown = "[ ]($currentServer/$chatRoomType/$room?msg=$id) $mention ", replyMarkdown = "[ ]($currentServer/$chatRoomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map( quotedMessage = mapper.map(
message, RoomUiModel( message, RoomUiModel(
roles = chatRoles, roles = chatRoles,
isBroadcast = chatIsBroadcast isBroadcast = chatIsBroadcast
) )
).last().preview?.message ?: "" ).last().preview?.message ?: ""
) )
} }
...@@ -868,7 +868,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -868,7 +868,7 @@ class ChatRoomPresenter @Inject constructor(
} }
it.chatRoom.name == name || it.chatRoom.fullname == name it.chatRoom.name == name || it.chatRoom.fullname == name
}.map { }.map {
with (it.chatRoom) { with(it.chatRoom) {
ChatRoom( ChatRoom(
id = id, id = id,
subscriptionId = subscriptionId, subscriptionId = subscriptionId,
...@@ -1008,6 +1008,20 @@ class ChatRoomPresenter @Inject constructor( ...@@ -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) { fun runCommand(text: String, roomId: String) {
launchUI(strategy) { launchUI(strategy) {
try { try {
...@@ -1103,8 +1117,8 @@ class ChatRoomPresenter @Inject constructor( ...@@ -1103,8 +1117,8 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.map( val viewModelStreamedMessage = mapper.map(
streamedMessage, RoomUiModel( streamedMessage, RoomUiModel(
roles = chatRoles, isBroadcast = chatIsBroadcast, isRoom = true roles = chatRoles, isBroadcast = chatIsBroadcast, isRoom = true
) )
) )
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId) val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
...@@ -1126,4 +1140,33 @@ class ChatRoomPresenter @Inject constructor( ...@@ -1126,4 +1140,33 @@ class ChatRoomPresenter @Inject constructor(
navigator.toMessageInformation(messageId = messageId) 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 ...@@ -3,6 +3,7 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.uimodel.BaseUiModel import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel 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.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
...@@ -127,6 +128,9 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -127,6 +128,9 @@ interface ChatRoomView : LoadingView, MessageView {
fun populatePeopleSuggestions(members: List<PeopleSuggestionUiModel>) fun populatePeopleSuggestions(members: List<PeopleSuggestionUiModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionUiModel>) fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionUiModel>)
fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>)
/** /**
* This user has joined the chat callback. * This user has joined the chat callback.
* *
......
...@@ -6,13 +6,11 @@ import android.content.ClipData ...@@ -6,13 +6,11 @@ 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.content.res.Configuration
import android.graphics.drawable.Drawable 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
...@@ -37,6 +35,7 @@ import chat.rocket.android.analytics.AnalyticsManager ...@@ -37,6 +35,7 @@ import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter 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.PEOPLE
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
...@@ -47,6 +46,7 @@ import chat.rocket.android.chatroom.uimodel.BaseUiModel ...@@ -47,6 +46,7 @@ import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.MessageUiModel import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel 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.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.draw.main.ui.DRAWING_BYTE_ARRAY_EXTRA_DATA import chat.rocket.android.draw.main.ui.DRAWING_BYTE_ARRAY_EXTRA_DATA
import chat.rocket.android.draw.main.ui.DrawingActivity import chat.rocket.android.draw.main.ui.DrawingActivity
...@@ -63,7 +63,6 @@ import chat.rocket.android.helper.ImageHelper ...@@ -63,7 +63,6 @@ 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
...@@ -76,8 +75,6 @@ import chat.rocket.android.util.extensions.ui ...@@ -76,8 +75,6 @@ import chat.rocket.android.util.extensions.ui
import chat.rocket.common.model.RoomType 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 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
...@@ -86,8 +83,6 @@ import kotlinx.android.synthetic.main.fragment_chat_room.* ...@@ -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_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
...@@ -258,6 +253,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -258,6 +253,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
recycler_view.removeOnLayoutChangeListener(layoutChangeListener) recycler_view.removeOnLayoutChangeListener(layoutChangeListener)
presenter.disconnect() presenter.disconnect()
presenter.saveUnfinishedMessage(chatRoomId, text_message.text.toString())
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
unsubscribeComposeTextMessage() unsubscribeComposeTextMessage()
...@@ -476,15 +472,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -476,15 +472,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
ui { ui {
when (usernameList.size) { when (usernameList.size) {
1 -> text_typing_status.text = 1 -> text_typing_status.text =
SpannableStringBuilder() SpannableStringBuilder()
.bold { append(usernameList[0]) } .bold { append(usernameList[0]) }
.append(getString(R.string.msg_is_typing)) .append(getString(R.string.msg_is_typing))
2 -> text_typing_status.text = 2 -> text_typing_status.text =
SpannableStringBuilder() SpannableStringBuilder()
.bold { append(usernameList[0]) } .bold { append(usernameList[0]) }
.append(getString(R.string.msg_and)) .append(getString(R.string.msg_and))
.bold { append(usernameList[1]) } .bold { append(usernameList[1]) }
.append(getString(R.string.msg_are_typing)) .append(getString(R.string.msg_are_typing))
else -> text_typing_status.text = getString(R.string.msg_several_users_are_typing) else -> text_typing_status.text = getString(R.string.msg_several_users_are_typing)
} }
...@@ -562,11 +558,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -562,11 +558,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun showReplyingAction( override fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String) {
username: String,
replyMarkdown: String,
quotedMessage: String
) {
ui { ui {
citation = replyMarkdown citation = replyMarkdown
actionSnackbar.title = username actionSnackbar.title = username
...@@ -616,6 +608,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -616,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) { override fun copyToClipboard(message: String) {
ui { ui {
val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
...@@ -775,10 +773,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -775,10 +773,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
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 {
println("attach")
}
activity?.supportFragmentManager?.registerFragmentLifecycleCallbacks( activity?.supportFragmentManager?.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() { object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached( override fun onFragmentAttached(
...@@ -796,6 +790,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -796,6 +790,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
) )
subscribeComposeTextMessage() subscribeComposeTextMessage()
getUnfinishedMessage()
emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container)) emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
emojiKeyboardPopup.listener = this emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener { text_message.listener = object : ComposerEditText.ComposerEditTextListener {
...@@ -842,16 +837,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -842,16 +837,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}, 400) }, 400)
} }
button_add_reaction.setOnClickListener { view -> button_add_reaction.setOnClickListener { _ ->
openEmojiKeyboardPopup() openEmojiKeyboardPopup()
} }
button_drawing.setOnClickListener { button_drawing.setOnClickListener {
activity?.let { activity?.let { fragmentActivity ->
if (!ImageHelper.canWriteToExternalStorage(it)) { if (!ImageHelper.canWriteToExternalStorage(fragmentActivity)) {
ImageHelper.checkWritingPermission(it) ImageHelper.checkWritingPermission(fragmentActivity)
} else { } else {
val intent = Intent(it, DrawingActivity::class.java) val intent = Intent(fragmentActivity, DrawingActivity::class.java)
startActivityForResult(intent, REQUEST_CODE_FOR_DRAW) startActivityForResult(intent, REQUEST_CODE_FOR_DRAW)
} }
} }
...@@ -863,12 +858,26 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -863,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() { private fun setupSuggestionsView() {
suggestions_view.anchorTo(text_message) suggestions_view.anchorTo(text_message)
.setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height)) .setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height))
.addTokenAdapter(PeopleSuggestionsAdapter(context!!)) .addTokenAdapter(PeopleSuggestionsAdapter(context!!))
.addTokenAdapter(CommandSuggestionsAdapter()) .addTokenAdapter(CommandSuggestionsAdapter())
.addTokenAdapter(RoomSuggestionsAdapter()) .addTokenAdapter(RoomSuggestionsAdapter())
.addTokenAdapter(EmojiSuggestionsAdapter())
.addSuggestionProviderAction("@") { query -> .addSuggestionProviderAction("@") { query ->
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
presenter.spotlight(query, PEOPLE, true) presenter.spotlight(query, PEOPLE, true)
...@@ -879,10 +888,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -879,10 +888,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
presenter.loadChatRooms() presenter.loadChatRooms()
} }
} }
.addSuggestionProviderAction("/") { _ -> .addSuggestionProviderAction("/") {
presenter.loadCommands() presenter.loadCommands()
} }
.addSuggestionProviderAction(":") {
presenter.loadEmojis()
}
presenter.loadEmojis()
presenter.loadCommands() 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)
}
}
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)"
}
}
...@@ -4,6 +4,7 @@ import android.app.Activity ...@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
...@@ -50,4 +51,4 @@ object KeyboardHelper { ...@@ -50,4 +51,4 @@ object KeyboardHelper {
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.SHOW_IMPLICIT) inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.SHOW_IMPLICIT)
} }
} }
} }
\ No newline at end of file
...@@ -5,6 +5,7 @@ import android.content.Context ...@@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.text.Spannable
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.ImageSpan
...@@ -13,6 +14,7 @@ import android.util.Patterns ...@@ -13,6 +14,7 @@ import android.util.Patterns
import android.view.View import android.view.View
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.StrikethroughDelimiterProcessor
import chat.rocket.android.emoji.EmojiParser import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiRepository import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.EmojiTypefaceSpan import chat.rocket.android.emoji.EmojiTypefaceSpan
...@@ -21,16 +23,23 @@ import chat.rocket.android.server.domain.useRealName ...@@ -21,16 +23,23 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.openTabbedUrl import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.common.model.SimpleUser import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message 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.AbstractVisitor
import org.commonmark.node.Document import org.commonmark.node.Document
import org.commonmark.node.Emphasis
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.StrongEmphasis
import org.commonmark.node.Text import org.commonmark.node.Text
import ru.noties.markwon.Markwon import org.commonmark.parser.Parser
import ru.noties.markwon.SpannableBuilder import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import ru.noties.markwon.tasklist.TaskListExtension
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class MessageParser @Inject constructor( class MessageParser @Inject constructor(
...@@ -39,8 +48,6 @@ class MessageParser @Inject constructor( ...@@ -39,8 +48,6 @@ class MessageParser @Inject constructor(
private val settings: PublicSettings private val settings: PublicSettings
) { ) {
private val parser = Markwon.createParser()
/** /**
* Render markdown and other rules on message to rich text with spans. * Render markdown and other rules on message to rich text with spans.
* *
...@@ -52,6 +59,16 @@ class MessageParser @Inject constructor( ...@@ -52,6 +59,16 @@ class MessageParser @Inject constructor(
fun render(message: Message, selfUsername: String? = null): CharSequence { fun render(message: Message, selfUsername: String? = null): CharSequence {
var text: String = message.message var text: String = message.message
val mentions = mutableListOf<String>() val mentions = mutableListOf<String>()
val parser = Parser.Builder()
.extensions(Arrays.asList<Extension>(
StrikethroughExtension.create(),
TablesExtension.create(),
TaskListExtension.create()
))
.customDelimiterProcessor(StrikethroughDelimiterProcessor())
.build()
message.mentions?.forEach { message.mentions?.forEach {
val mention = getMention(it) val mention = getMention(it)
mentions.add(mention) mentions.add(mention)
...@@ -59,12 +76,17 @@ class MessageParser @Inject constructor( ...@@ -59,12 +76,17 @@ class MessageParser @Inject constructor(
text = text.replace("@${it.username}", mention) text = text.replace("@${it.username}", mention)
} }
} }
val builder = SpannableBuilder() val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text) 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(MarkdownVisitor(configuration, builder))
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(context, 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))
} }
...@@ -72,13 +94,6 @@ class MessageParser @Inject constructor( ...@@ -72,13 +94,6 @@ class MessageParser @Inject constructor(
return builder.text() 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 { private fun getMention(user: SimpleUser): String {
return if (settings.useRealName()) { return if (settings.useRealName()) {
user.name ?: "@${user.username}" user.name ?: "@${user.username}"
...@@ -87,6 +102,31 @@ class MessageParser @Inject constructor( ...@@ -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( class MentionVisitor(
context: Context, context: Context,
private val builder: SpannableBuilder, private val builder: SpannableBuilder,
...@@ -98,28 +138,28 @@ class MessageParser @Inject constructor( ...@@ -98,28 +138,28 @@ class MessageParser @Inject constructor(
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme) 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 myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.colorWhite, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, 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 padding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat() private val radius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
override fun visit(t: Text) { override fun visit(document: Document) {
val text = t.literal val text = builder.text()
val mentionsList = mentions.toMutableList().also { val mentionsList = mentions.toMutableList().also {
it.add("@all") it.add("@all")
it.add("@here") it.add("@here")
}.toList() }.distinct()
mentionsList.forEach { mentionsList.forEach {
val mentionMe = it == currentUser || it == "@all" || it == "@here" 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) { while (offset > -1) {
val textColor = if (mentionMe) myselfTextColor else othersTextColor val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding, val usernameSpan = MentionSpan(backgroundColor, textColor, radius, padding,
mentionMe) mentionMe)
// Add 1 to end offset to include the @. // Add 1 to end offset to include the @.
val end = offset + it.length + 1 val end = offset + it.length
builder.setSpan(usernameSpan, offset, end, 0) builder.setSpan(usernameSpan, offset, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
offset = text.indexOf("@$it", end, true) offset = text.indexOf(string = "@$it", startIndex = end, ignoreCase = false)
} }
} }
} }
...@@ -179,7 +219,7 @@ class MessageParser @Inject constructor( ...@@ -179,7 +219,7 @@ class MessageParser @Inject constructor(
} }
private fun newLine() { private fun newLine() {
if (builder.length() > 0 && '\n' != builder.lastChar()) { if (builder.isNotEmpty() && '\n' != builder.lastChar()) {
builder.append('\n') builder.append('\n')
} }
} }
...@@ -204,7 +244,6 @@ class MessageParser @Inject constructor( ...@@ -204,7 +244,6 @@ class MessageParser @Inject constructor(
consumed.add(link) consumed.add(link)
} }
} }
visitChildren(text)
} }
} }
......
...@@ -27,6 +27,7 @@ interface LocalRepository { ...@@ -27,6 +27,7 @@ interface LocalRepository {
const val SETTINGS_KEY = "settings_" const val SETTINGS_KEY = "settings_"
const val PERMISSIONS_KEY = "permissions_" const val PERMISSIONS_KEY = "permissions_"
const val USER_KEY = "user_" const val USER_KEY = "user_"
const val UNFINISHED_MSG_KEY = "unfinished_msg_"
const val CURRENT_USERNAME_KEY = "username_" const val CURRENT_USERNAME_KEY = "username_"
const val LAST_CHATROOMS_REFRESH = "_chatrooms_refresh" const val LAST_CHATROOMS_REFRESH = "_chatrooms_refresh"
} }
......
...@@ -5,13 +5,14 @@ ...@@ -5,13 +5,14 @@
android:id="@+id/attachment_container" android:id="@+id/attachment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="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: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 <View
android:id="@+id/quote_bar" android:id="@+id/quote_bar"
...@@ -19,7 +20,7 @@ ...@@ -19,7 +20,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="56dp" android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_gray_bar" 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_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
...@@ -28,11 +29,11 @@ ...@@ -28,11 +29,11 @@
style="@style/Sender.Name.TextView" style="@style/Sender.Name.TextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorPrimary" android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins"
app:layout_constraintStart_toEndOf="@+id/quote_bar" app:layout_constraintStart_toEndOf="@+id/quote_bar"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="8dp" /> tools:text="Ronald Perkins" />
<TextView <TextView
android:id="@+id/text_message_time" android:id="@+id/text_message_time"
...@@ -40,28 +41,39 @@ ...@@ -40,28 +41,39 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" 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_constraintStart_toEndOf="@+id/text_sender"
app:layout_constraintTop_toTopOf="@+id/text_sender" app:layout_constraintTop_toTopOf="@+id/text_sender"
app:layout_constraintBottom_toBottomOf="@+id/text_sender"/> tools:text="11:45 PM" />
<TextView <TextView
android:id="@+id/text_content" android:id="@+id/text_content"
style="@style/Message.Quote.TextView" style="@style/Message.Quote.TextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" app:layout_constraintBottom_toTopOf="@id/text_view_more"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_sender" app:layout_constraintStart_toStartOf="@+id/text_sender"
app:layout_constraintTop_toBottomOf="@+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!" /> 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 <include
layout="@layout/layout_reactions" layout="@layout/layout_reactions"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/quote_bar" 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> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="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>
...@@ -142,6 +142,10 @@ ...@@ -142,6 +142,10 @@
<string name="msg_message_copied">Nachricht kopiert</string> <string name="msg_message_copied">Nachricht kopiert</string>
<string name="msg_delete_message">Lösche Nachricht</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> <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 --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation --> <string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
......
...@@ -139,6 +139,10 @@ ...@@ -139,6 +139,10 @@
<string name="msg_member_already_added">Ya has seleccionado este usuario</string> <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_member_not_found">Miembro no encontrado</string>
<string name="msg_channel_created_successfully">Canal creado con éxito</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 --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation --> <string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
......
...@@ -131,6 +131,10 @@ ...@@ -131,6 +131,10 @@
<string name="msg_send">envoyer</string> <string name="msg_send">envoyer</string>
<string name="msg_delete_message">Supprimer Message</string> <string name="msg_delete_message">Supprimer Message</string>
<string name="msg_delete_description">Êtes-vous sûr de vouloir supprimer ce message</string> <string name="msg_delete_description">Êtes-vous sûr de vouloir supprimer ce message</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>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Privé</string> <string name="msg_private_channel">Privé</string>
......
...@@ -145,6 +145,10 @@ ...@@ -145,6 +145,10 @@
<string name="msg_member_already_added">आपने पहले से ही इस यूजर को चुन चुके है।</string> <string name="msg_member_already_added">आपने पहले से ही इस यूजर को चुन चुके है।</string>
<string name="msg_member_not_found">सदस्य नहीं मिला</string> <string name="msg_member_not_found">सदस्य नहीं मिला</string>
<string name="msg_channel_created_successfully">चैनल सफलतापूर्वक बनाया गया</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 --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">एनालिटिक्स ट्रैकिंग</string> <string name="msg_analytics_tracking">एनालिटिक्स ट्रैकिंग</string>
......
...@@ -132,6 +132,10 @@ ...@@ -132,6 +132,10 @@
<string name="msg_file_description">ファイルの説明</string> <string name="msg_file_description">ファイルの説明</string>
<string name="msg_send">送信</string> <string name="msg_send">送信</string>
<string name="msg_sent_attachment">添付ファイルを送信しました</string> <string name="msg_sent_attachment">添付ファイルを送信しました</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>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">プライベート</string> <string name="msg_private_channel">プライベート</string>
......
...@@ -130,9 +130,10 @@ ...@@ -130,9 +130,10 @@
<string name="msg_upload_file">Subir arquivo</string> <string name="msg_upload_file">Subir arquivo</string>
<string name="msg_file_description">Descrição do arquivo</string> <string name="msg_file_description">Descrição do arquivo</string>
<string name="msg_send">Enviar</string> <string name="msg_send">Enviar</string>
// TODO: Add proper translation. <string name="msg_delete_message">Remove mensagem</string>
<string name="msg_delete_message">Delete Message</string> <string name="msg_delete_description">Tem certeza que quer apagar esta mensagem?</string>
<string name="msg_delete_description">Are you sure you want to delete this message</string> <string name="msg_view_more">visualizar mais</string>
<string name="msg_view_less">visualizar menos</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Privado</string> <string name="msg_private_channel">Privado</string>
......
...@@ -130,6 +130,10 @@ ...@@ -130,6 +130,10 @@
<string name="msg_delete_description">Вы уверены, что хотите удалить это сообщение?</string> <string name="msg_delete_description">Вы уверены, что хотите удалить это сообщение?</string>
<string name="msg_channel_name">Название канала</string> <string name="msg_channel_name">Название канала</string>
<string name="msg_search">Поиск</string> <string name="msg_search">Поиск</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>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Приватный</string> <string name="msg_private_channel">Приватный</string>
......
...@@ -129,6 +129,10 @@ ...@@ -129,6 +129,10 @@
<string name="msg_send">Надіслати</string> <string name="msg_send">Надіслати</string>
<string name="msg_delete_message">Видалити повідомлення</string> <string name="msg_delete_message">Видалити повідомлення</string>
<string name="msg_delete_description">Ви впевнені, що хочете видалити це повідомлення?</string> <string name="msg_delete_description">Ви впевнені, що хочете видалити це повідомлення?</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>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Приватний</string> <string name="msg_private_channel">Приватний</string>
......
...@@ -44,4 +44,6 @@ ...@@ -44,4 +44,6 @@
<dimen name="viewer_toolbar_padding">16dp</dimen> <dimen name="viewer_toolbar_padding">16dp</dimen>
<dimen name="viewer_toolbar_title">16sp</dimen> <dimen name="viewer_toolbar_title">16sp</dimen>
<dimen name="quote_collapsed_height">32dp</dimen>
</resources> </resources>
\ No newline at end of file
...@@ -159,6 +159,8 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -159,6 +159,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_message_copied">Message copied</string>
<string name="msg_delete_message">Delete Message</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_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 --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <string name="msg_analytics_tracking">Analytics tracking</string>
......
...@@ -48,7 +48,7 @@ ext { ...@@ -48,7 +48,7 @@ ext {
kotshi : '1.0.4', kotshi : '1.0.4',
frescoImageViewer : '0.5.1', frescoImageViewer : '0.5.1',
markwon : '1.1.1', markwon : '2.0.0',
aVLoadingIndicatorView: '2.1.3', aVLoadingIndicatorView: '2.1.3',
glide : '4.8.0', glide : '4.8.0',
......
...@@ -146,7 +146,7 @@ object EmojiRepository { ...@@ -146,7 +146,7 @@ object EmojiRepository {
* *
* @return All emojis for all categories. * @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() return@withContext db.emojiDao().loadAllEmojis()
} }
......
package yampsample.leonardoaramaki.github.com.suggestions;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("yampsample.leonardoaramaki.github.com.suggestions.test", appContext.getPackageName());
}
}
package chat.rocket.android.suggestions.model package chat.rocket.android.suggestions.model
abstract class SuggestionModel(val text: String, // This is the text key for searches, must be unique. abstract class SuggestionModel(
val searchList: List<String> = emptyList(), // Where to search for matches. val text: String, // This is the text key for searches, must be unique.
val pinned: Boolean = false /* If pinned item will have priority to show */) { 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is SuggestionModel) return false if (other !is SuggestionModel) return false
......
...@@ -18,7 +18,9 @@ class TrieCompletionStrategy : CompletionStrategy { ...@@ -18,7 +18,9 @@ class TrieCompletionStrategy : CompletionStrategy {
return item 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>) { override fun addAll(list: List<SuggestionModel>) {
items.addAll(list) items.addAll(list)
......
...@@ -34,10 +34,7 @@ internal class Trie { ...@@ -34,10 +34,7 @@ internal class Trie {
val sanitizedWord = word.trim().toLowerCase() val sanitizedWord = word.trim().toLowerCase()
var current = root var current = root
sanitizedWord.forEach { ch -> sanitizedWord.forEach { ch ->
val child = current.getChild(ch) val child = current.getChild(ch) ?: return false
if (child == null) {
return false
}
current = child current = child
} }
if (current.isLeaf) { if (current.isLeaf) {
...@@ -63,7 +60,7 @@ internal class Trie { ...@@ -63,7 +60,7 @@ internal class Trie {
lastNode = lastNode?.getChild(ch) lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList() if (lastNode == null) return emptyList()
} }
return lastNode!!.getItems() return lastNode!!.getItems().take(5).toList()
} }
fun getCount() = count fun getCount() = count
......
package chat.rocket.android.suggestions.strategy.trie.data package chat.rocket.android.suggestions.strategy.trie.data
import chat.rocket.android.suggestions.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>() val children = hashMapOf<Char, TrieNode>()
fun getChild(c: Char): TrieNode? { fun getChild(c: Char): TrieNode? {
...@@ -28,19 +32,17 @@ internal class TrieNode(internal var data: Char, ...@@ -28,19 +32,17 @@ internal class TrieNode(internal var data: Char,
return list return list
} }
class X : SuggestionModel("") fun getItems(): Sequence<SuggestionModel> = buildSequence {
fun getItems(): List<SuggestionModel> {
val list = arrayListOf<SuggestionModel>()
if (isLeaf) { if (isLeaf) {
list.add(item!!) yield(item!!)
} }
children.forEach { node -> children.forEach { node ->
node.value.let { node.value.let {
list.addAll(it.getItems()) yieldAll(it.getItems())
} }
} }
return list
} }
override fun toString(): String = if (parent == null) "" else "${parent.toString()}$data" override fun toString(): String = if (parent == null) "" else "${parent.toString()}$data"
......
...@@ -10,17 +10,9 @@ import kotlin.properties.Delegates ...@@ -10,17 +10,9 @@ import kotlin.properties.Delegates
abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
val token: String, val token: String,
val constraint: Int = CONSTRAINT_UNBOUND, val constraint: Int = CONSTRAINT_UNBOUND,
threshold: Int = MAX_RESULT_COUNT) : RecyclerView.Adapter<VH>() { completionStrategy: CompletionStrategy? = null,
companion object { threshold: Int = MAX_RESULT_COUNT
// Any number of results. ) : RecyclerView.Adapter<VH>() {
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
}
private var itemType: Type? = null private var itemType: Type? = null
private var itemClickListener: ItemClickListener? = null private var itemClickListener: ItemClickListener? = null
...@@ -30,7 +22,7 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( ...@@ -30,7 +22,7 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
// Maximum number of results/suggestions to display. // Maximum number of results/suggestions to display.
private var resultsThreshold: Int = if (threshold > 0) threshold else RESULT_COUNT_UNLIMITED private var resultsThreshold: Int = if (threshold > 0) threshold else RESULT_COUNT_UNLIMITED
// The strategy used for suggesting completions. // 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. // 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) val items = strategy.autocompleteItems(newTerm)
...@@ -105,4 +97,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( ...@@ -105,4 +97,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
interface ItemClickListener { interface ItemClickListener {
fun onClick(item: SuggestionModel) 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
}
}
...@@ -149,15 +149,14 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -149,15 +149,14 @@ class SuggestionsView : FrameLayout, TextWatcher {
} }
fun addTokenAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView { fun addTokenAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
adaptersByToken.getOrPut(adapter.token, { adapter }) adaptersByToken.getOrPut(adapter.token) { adapter }
return this return this
} }
fun addItems(token: String, list: List<SuggestionModel>): SuggestionsView { fun addItems(token: String, list: List<SuggestionModel>): SuggestionsView {
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
val adapter = adapter(token) val adapter = adapter(token)
localProvidersByToken.getOrPut(token, { hashMapOf() }) localProvidersByToken.getOrPut(token) { hashMapOf() }.put(adapter.term(), list)
.put(adapter.term(), list)
if (completionOffset.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand() if (completionOffset.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand()
adapter.addItems(list) adapter.addItems(list)
} }
......
package yampsample.leonardoaramaki.github.com.suggestions;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment