Unverified Commit e3989237 authored by divyanshu bhargava's avatar divyanshu bhargava Committed by GitHub

Merge branch 'develop-2.x' into lang-hindi

parents 5f4cce90 2176c0ce
...@@ -24,7 +24,6 @@ import chat.rocket.android.server.domain.* ...@@ -24,7 +24,6 @@ import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.widget.emoji.EmojiRepository import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull
import chat.rocket.core.model.Value import chat.rocket.core.model.Value
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore import com.crashlytics.android.core.CrashlyticsCore
......
...@@ -49,6 +49,10 @@ class ChatRoomAdapter( ...@@ -49,6 +49,10 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.message_url_preview) val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener, reactionListener) UrlPreviewViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.MESSAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -87,6 +91,7 @@ class ChatRoomAdapter( ...@@ -87,6 +91,7 @@ class ChatRoomAdapter(
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel) is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel) is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel) is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
} }
} }
...@@ -117,19 +122,22 @@ class ChatRoomAdapter( ...@@ -117,19 +122,22 @@ class ChatRoomAdapter(
fun updateItem(message: BaseViewModel<*>) { fun updateItem(message: BaseViewModel<*>) {
var index = dataSet.indexOfLast { it.messageId == message.messageId } var index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfFirst = dataSet.indexOfFirst { it.messageId == message.messageId } val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index") Timber.d("index: $index")
if (index > -1) { if (index > -1) {
dataSet[index] = message dataSet[index] = message
notifyItemChanged(index) dataSet.forEachIndexed { index, viewModel ->
while (dataSet[index].nextDownStreamMessage != null) { if (viewModel.messageId == message.messageId) {
dataSet[index].nextDownStreamMessage!!.reactions = message.reactions if (viewModel.nextDownStreamMessage == null) {
notifyItemChanged(--index) viewModel.reactions = message.reactions
}
notifyItemChanged(index)
}
} }
// Delete message only if current is a system message update, i.e.: Message Removed // Delete message only if current is a system message update, i.e.: Message Removed
if (message.message.isSystemMessage() && indexOfFirst > -1 && indexOfFirst != index) { if (message.message.isSystemMessage() && indexOfNext > -1 && indexOfNext != index) {
dataSet.removeAt(indexOfFirst) dataSet.removeAt(indexOfNext)
notifyItemRemoved(indexOfFirst) notifyItemRemoved(indexOfNext)
} }
} }
} }
......
...@@ -12,7 +12,7 @@ import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder ...@@ -12,7 +12,7 @@ import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class CommandSuggestionsAdapter : SuggestionsAdapter<CommandSuggestionsViewHolder>(token = "/", class CommandSuggestionsAdapter : SuggestionsAdapter<CommandSuggestionsViewHolder>(token = "/",
constraint = CONSTRAINT_BOUND_TO_START, threshold = UNLIMITED_RESULT_COUNT) { constraint = CONSTRAINT_BOUND_TO_START, threshold = RESULT_COUNT_UNLIMITED) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommandSuggestionsViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommandSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_command_item, parent, val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_command_item, parent,
......
package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message.view.*
class MessageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
}
}
override fun bindViews(data: MessageAttachmentViewModel) {
with(itemView) {
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import DrawableHelper import DrawableHelper
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
...@@ -13,10 +14,32 @@ import chat.rocket.android.util.extensions.setVisible ...@@ -13,10 +14,32 @@ import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.common.model.UserStatus
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") { class PeopleSuggestionsAdapter(context: Context) : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
init {
val allDescription = context.getString(R.string.suggest_all_description)
val hereDescription = context.getString(R.string.suggest_here_description)
val pinnedList = listOf(
PeopleSuggestionViewModel(imageUri = null,
text = "all",
username = "all",
name = allDescription,
status = null,
pinned = false,
searchList = listOf("all")),
PeopleSuggestionViewModel(imageUri = null,
text = "here",
username = "here",
name = hereDescription,
status = null,
pinned = false,
searchList = listOf("here"))
)
setPinnedSuggestions(pinnedList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent, val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent,
false) false)
...@@ -34,15 +57,19 @@ class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>( ...@@ -34,15 +57,19 @@ class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>(
val statusView = itemView.findViewById<ImageView>(R.id.image_status) val statusView = itemView.findViewById<ImageView>(R.id.image_status)
username.text = item.username username.text = item.username
name.text = item.name name.text = item.name
if (item.imageUri.isEmpty()) { if (item.imageUri?.isEmpty() != false) {
avatar.setVisible(false) avatar.setVisible(false)
} else { } else {
avatar.setVisible(true) avatar.setVisible(true)
avatar.setImageURI(item.imageUri) avatar.setImageURI(item.imageUri)
} }
val status = item.status ?: UserStatus.Offline() val status = item.status
val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context) if (status != null) {
statusView.setImageDrawable(statusDrawable) val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context)
statusView.setImageDrawable(statusDrawable)
} else {
statusView.setVisible(false)
}
setOnClickListener { setOnClickListener {
itemClickListener?.onClick(item) itemClickListener?.onClick(item)
} }
......
...@@ -291,7 +291,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -291,7 +291,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
view.showReplyingAction( view.showReplyingAction(
username = user, username = user,
replyMarkdown = "[ ]($serverUrl/$room/$roomName?msg=$id) $mention ", replyMarkdown = "[ ]($serverUrl/$room/$roomName?msg=$id) $mention ",
quotedMessage = m.message quotedMessage = mapper.map(message).last().preview?.message ?: ""
) )
} }
} }
...@@ -499,7 +499,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -499,7 +499,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
//TODO: cache the commands //TODO: cache the commands
val commands = client.commands(0, 100).result val commands = client.commands(0, 100).result
view.populateCommandSuggestions(commands.map { view.populateCommandSuggestions(commands.map {
println("${it.command} - ${it.description}")
CommandSuggestionViewModel(it.command, it.description ?: "", listOf(it.command)) CommandSuggestionViewModel(it.command, it.description ?: "", listOf(it.command))
}) })
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
......
package chat.rocket.android.chatroom.ui package chat.rocket.android.chatroom.ui
import android.graphics.drawable.Drawable
import android.support.design.widget.BaseTransientBottomBar import android.support.design.widget.BaseTransientBottomBar
import android.support.v4.view.ViewCompat import android.support.v4.view.ViewCompat
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
...@@ -27,7 +27,6 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> { ...@@ -27,7 +27,6 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
actionSnackbar.cancelView = view.findViewById(R.id.image_view_action_cancel_quote) as ImageView actionSnackbar.cancelView = view.findViewById(R.id.image_view_action_cancel_quote) as ImageView
actionSnackbar.duration = BaseTransientBottomBar.LENGTH_INDEFINITE actionSnackbar.duration = BaseTransientBottomBar.LENGTH_INDEFINITE
val spannable = Markwon.markdown(context, content).trim() val spannable = Markwon.markdown(context, content).trim()
actionSnackbar.marginDrawable = context.getDrawable(R.drawable.quote)
actionSnackbar.messageTextView.content = spannable actionSnackbar.messageTextView.content = spannable
return actionSnackbar return actionSnackbar
} }
...@@ -37,19 +36,16 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> { ...@@ -37,19 +36,16 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
lateinit var cancelView: View lateinit var cancelView: View
private lateinit var messageTextView: TextView private lateinit var messageTextView: TextView
private lateinit var titleTextView: TextView private lateinit var titleTextView: TextView
private lateinit var marginDrawable: Drawable
var text: String = "" var text: String = ""
set(value) { set(value) {
val spannable = parser.renderMarkdown(value) as Spannable val spannable = SpannableStringBuilder.valueOf(value)
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
messageTextView.content = spannable messageTextView.content = spannable
} }
var title: String = "" var title: String = ""
set(value) { set(value) {
val spannable = Markwon.markdown(this.context, value) as Spannable val spannable = Markwon.markdown(this.context, value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
titleTextView.content = spannable titleTextView.content = spannable
} }
...@@ -68,17 +64,17 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> { ...@@ -68,17 +64,17 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
override fun animateContentOut(delay: Int, duration: Int) { override fun animateContentOut(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 1f) ViewCompat.setScaleY(content, 1f)
ViewCompat.animate(content) ViewCompat.animate(content)
.scaleY(0f) .scaleY(0f)
.setDuration(duration.toLong()) .setDuration(duration.toLong())
.startDelay = delay.toLong() .startDelay = delay.toLong()
} }
override fun animateContentIn(delay: Int, duration: Int) { override fun animateContentIn(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 0f) ViewCompat.setScaleY(content, 0f)
ViewCompat.animate(content) ViewCompat.animate(content)
.scaleY(1f) .scaleY(1f)
.setDuration(duration.toLong()) .setDuration(duration.toLong())
.startDelay = delay.toLong() .startDelay = delay.toLong()
} }
} }
} }
\ No newline at end of file
...@@ -36,7 +36,9 @@ import kotlinx.android.synthetic.main.message_attachment_options.* ...@@ -36,7 +36,9 @@ 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 timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue
fun newInstance(chatRoomId: String, fun newInstance(chatRoomId: String,
chatRoomName: String, chatRoomName: String,
...@@ -88,6 +90,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -88,6 +90,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private val centerX by lazy { recycler_view.right } private val centerX by lazy { recycler_view.right }
private val centerY by lazy { recycler_view.bottom } private val centerY by lazy { recycler_view.bottom }
private val handler = Handler() private val handler = Handler()
private var verticalScrollOffset = AtomicInteger(0)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -207,13 +210,54 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -207,13 +210,54 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
}) })
} }
recycler_view.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
val y = oldBottom - bottom
if (y.absoluteValue > 0) {
// if y is positive the keyboard is up else it's down
recycler_view.post {
if (y > 0 || verticalScrollOffset.get().absoluteValue >= y.absoluteValue) {
recycler_view.scrollBy(0, y)
} else {
recycler_view.scrollBy(0, verticalScrollOffset.get())
}
}
}
}
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var state = AtomicInteger(RecyclerView.SCROLL_STATE_IDLE)
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
state.compareAndSet(RecyclerView.SCROLL_STATE_IDLE, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
if (!state.compareAndSet(RecyclerView.SCROLL_STATE_SETTLING, newState)) {
state.compareAndSet(RecyclerView.SCROLL_STATE_DRAGGING, newState)
}
}
RecyclerView.SCROLL_STATE_DRAGGING -> {
state.compareAndSet(RecyclerView.SCROLL_STATE_IDLE, newState)
}
RecyclerView.SCROLL_STATE_SETTLING -> {
state.compareAndSet(RecyclerView.SCROLL_STATE_DRAGGING, newState)
}
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (state.get() != RecyclerView.SCROLL_STATE_IDLE) {
verticalScrollOffset.getAndAdd(dy)
}
}
})
} }
val oldMessagesCount = adapter.itemCount val oldMessagesCount = adapter.itemCount
adapter.appendData(dataSet) adapter.appendData(dataSet)
recycler_view.scrollToPosition(92)
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) { if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
} }
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true) presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
} }
...@@ -241,6 +285,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -241,6 +285,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun showNewMessage(message: List<BaseViewModel<*>>) { override fun showNewMessage(message: List<BaseViewModel<*>>) {
adapter.prependData(message) adapter.prependData(message)
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
} }
override fun disableSendMessageButton() { override fun disableSendMessageButton() {
...@@ -281,6 +326,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -281,6 +326,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (!recycler_view.isAtBottom()) { if (!recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) { if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
} }
} }
} }
...@@ -430,6 +476,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -430,6 +476,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setupFab() { private fun setupFab() {
button_fab.setOnClickListener { button_fab.setOnClickListener {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
button_fab.hide() button_fab.hide()
} }
} }
...@@ -453,11 +500,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -453,11 +500,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
emojiKeyboardPopup.listener = this emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener { text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardOpened() { override fun onKeyboardOpened() {
if (recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
}
}
} }
override fun onKeyboardClosed() { override fun onKeyboardClosed() {
...@@ -511,7 +553,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -511,7 +553,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
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()) .addTokenAdapter(PeopleSuggestionsAdapter(context!!))
.addTokenAdapter(CommandSuggestionsAdapter()) .addTokenAdapter(CommandSuggestionsAdapter())
.addTokenAdapter(RoomSuggestionsAdapter()) .addTokenAdapter(RoomSuggestionsAdapter())
.addSuggestionProviderAction("@") { query -> .addSuggestionProviderAction("@") { query ->
......
...@@ -12,8 +12,9 @@ data class AudioAttachmentViewModel( ...@@ -12,8 +12,9 @@ data class AudioAttachmentViewModel(
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
) : BaseFileAttachmentViewModel<AudioAttachment> { override var preview: Message? = null
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int override val layoutId: Int
......
...@@ -11,6 +11,7 @@ interface BaseViewModel<out T> { ...@@ -11,6 +11,7 @@ interface BaseViewModel<out T> {
val layoutId: Int val layoutId: Int
var reactions: List<ReactionViewModel> var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>? var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
enum class ViewType(val viewType: Int) { enum class ViewType(val viewType: Int) {
MESSAGE(0), MESSAGE(0),
......
...@@ -12,7 +12,8 @@ data class ImageAttachmentViewModel( ...@@ -12,7 +12,8 @@ data class ImageAttachmentViewModel(
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseFileAttachmentViewModel<ImageAttachment> { ) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
data class MessageAttachmentViewModel(
override val message: Message,
override val rawData: Message,
override val messageId: String,
var senderName: String,
val time: CharSequence,
val content: CharSequence,
val isPinned: Boolean,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
}
\ No newline at end of file
...@@ -14,6 +14,7 @@ data class MessageViewModel( ...@@ -14,6 +14,7 @@ data class MessageViewModel(
override val isPinned: Boolean, override val isPinned: Boolean,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean var isFirstUnread: Boolean
) : BaseMessageViewModel<Message> { ) : BaseMessageViewModel<Message> {
override val viewType: Int override val viewType: Int
......
...@@ -13,7 +13,8 @@ data class UrlPreviewViewModel( ...@@ -13,7 +13,8 @@ data class UrlPreviewViewModel(
val description: CharSequence?, val description: CharSequence?,
val thumbUrl: String?, val thumbUrl: String?,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseViewModel<Url> { ) : BaseViewModel<Url> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
...@@ -12,7 +12,8 @@ data class VideoAttachmentViewModel( ...@@ -12,7 +12,8 @@ data class VideoAttachmentViewModel(
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseFileAttachmentViewModel<VideoAttachment> { ) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
...@@ -3,7 +3,7 @@ package chat.rocket.android.chatroom.viewmodel.suggestion ...@@ -3,7 +3,7 @@ package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
class PeopleSuggestionViewModel(val imageUri: String, class PeopleSuggestionViewModel(val imageUri: String?,
text: String, text: String,
val username: String, val username: String,
val name: String, val name: String,
......
package chat.rocket.android.chatrooms.presentation package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.main.presentation.MainNavigator import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
...@@ -9,7 +10,10 @@ import chat.rocket.android.server.infraestructure.chatRooms ...@@ -9,7 +10,10 @@ import chat.rocket.android.server.infraestructure.chatRooms
import chat.rocket.android.server.infraestructure.state import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.* import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.User
import chat.rocket.core.internal.model.Subscription import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.StreamMessage import chat.rocket.core.internal.realtime.StreamMessage
...@@ -30,6 +34,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -30,6 +34,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val getChatRoomsInteractor: GetChatRoomsInteractor, private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor, private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val viewModelMapper: ViewModelMapper,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
factory: ConnectionManagerFactory) { factory: ConnectionManagerFactory) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!) private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
...@@ -89,9 +94,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -89,9 +94,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
val chatRoomsCombined = mutableListOf<ChatRoom>() val chatRoomsCombined = mutableListOf<ChatRoom>()
chatRoomsCombined.addAll(usersToChatRooms(users)) chatRoomsCombined.addAll(usersToChatRooms(users))
chatRoomsCombined.addAll(roomsToChatRooms(rooms)) chatRoomsCombined.addAll(roomsToChatRooms(rooms))
view.updateChatRooms(chatRoomsCombined) view.updateChatRooms(getChatRoomsWithPreviews(chatRoomsCombined.toList()))
} else { } else {
view.updateChatRooms(roomList) view.updateChatRooms(getChatRoomsWithPreviews(roomList))
} }
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
Timber.e(ex) Timber.e(ex)
...@@ -156,7 +161,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -156,7 +161,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
val sortedRooms = sortRooms(chatRooms) val sortedRooms = sortRooms(chatRooms)
Timber.d("Loaded rooms: ${sortedRooms.size}") Timber.d("Loaded rooms: ${sortedRooms.size}")
saveChatRoomsInteractor.save(currentServer, sortedRooms) saveChatRoomsInteractor.save(currentServer, sortedRooms)
return sortedRooms return getChatRoomsWithPreviews(sortedRooms)
} }
private fun sortRooms(chatRooms: List<ChatRoom>): List<ChatRoom> { private fun sortRooms(chatRooms: List<ChatRoom>): List<ChatRoom> {
...@@ -167,7 +172,17 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -167,7 +172,17 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private fun updateRooms() { private fun updateRooms() {
Timber.d("Updating Rooms") Timber.d("Updating Rooms")
launch(strategy.jobs) { launch(strategy.jobs) {
view.updateChatRooms(getChatRoomsInteractor.get(currentServer)) view.updateChatRooms(getChatRoomsWithPreviews(getChatRoomsInteractor.get(currentServer)))
}
}
private suspend fun getChatRoomsWithPreviews(chatRooms: List<ChatRoom>): List<ChatRoom> {
return chatRooms.map {
if (it.lastMessage != null) {
it.copy(lastMessage = viewModelMapper.map(it.lastMessage!!).last().preview)
} else {
it
}
} }
} }
...@@ -304,7 +319,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -304,7 +319,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
// Update a ChatRoom with a Subscription information // Update a ChatRoom with a Subscription information
private fun updateSubscription(subscription: Subscription) { private fun updateSubscription(subscription: Subscription) {
Timber.d("Updating subscrition: ${subscription.id} - ${subscription.name}") Timber.d("Updating subscription: ${subscription.id} - ${subscription.name}")
val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList() val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId } val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId }
chatRoom?.apply { chatRoom?.apply {
......
...@@ -3,14 +3,18 @@ package chat.rocket.android.chatrooms.ui ...@@ -3,14 +3,18 @@ package chat.rocket.android.chatrooms.ui
import DateTimeHelper import DateTimeHelper
import DrawableHelper import DrawableHelper
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.Color
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.PublicSettings import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.content import chat.rocket.android.util.extensions.content
...@@ -26,6 +30,7 @@ import kotlinx.android.synthetic.main.unread_messages_badge.view.* ...@@ -26,6 +30,7 @@ import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class ChatRoomsAdapter(private val context: Context, class ChatRoomsAdapter(private val context: Context,
private val settings: PublicSettings, private val settings: PublicSettings,
private val localRepository: LocalRepository,
private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() { private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
var dataSet: MutableList<ChatRoom> = ArrayList() var dataSet: MutableList<ChatRoom> = ArrayList()
...@@ -35,7 +40,7 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -35,7 +40,7 @@ class ChatRoomsAdapter(private val context: Context,
override fun getItemCount(): Int = dataSet.size override fun getItemCount(): Int = dataSet.size
fun updateRooms(newRooms: List<ChatRoom>) { fun updateRooms(newRooms: List<ChatRoom>) {
dataSet.clear() dataSet.clear()
dataSet.addAll(newRooms) dataSet.addAll(newRooms)
} }
...@@ -116,21 +121,30 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -116,21 +121,30 @@ class ChatRoomsAdapter(private val context: Context,
val lastMessageSender = lastMessage?.sender val lastMessageSender = lastMessage?.sender
if (lastMessage != null && lastMessageSender != null) { if (lastMessage != null && lastMessageSender != null) {
val message = lastMessage.message val message = lastMessage.message
val senderUsername = lastMessageSender.username val senderUsername = if (settings.useRealName()) {
lastMessageSender.name ?: lastMessageSender.username
} else {
lastMessageSender.username
}
when (senderUsername) { when (senderUsername) {
chatRoom.name -> { chatRoom.name -> {
textView.content = message textView.content = message
} }
// TODO Change to MySelf
// chatRoom.user?.username -> {
// holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message"
// }
else -> { else -> {
textView.content = "@$senderUsername: $message" val user = if (localRepository.checkIfMyself(lastMessageSender.username!!)) {
"${context.getString(R.string.msg_you)}: "
} else {
"$senderUsername: "
}
val spannable = SpannableStringBuilder(user)
val len = spannable.length
spannable.setSpan(ForegroundColorSpan(Color.BLACK), 0, len - 1, 0)
spannable.append(message)
textView.content = spannable
} }
} }
} else { } else {
textView.content = "" textView.content = context.getText(R.string.msg_no_messages_yet)
} }
} }
......
...@@ -12,6 +12,7 @@ import android.view.* ...@@ -12,6 +12,7 @@ import android.view.*
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
...@@ -31,6 +32,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -31,6 +32,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter @Inject lateinit var presenter: ChatRoomsPresenter
@Inject lateinit var serverInteractor: GetCurrentServerInteractor @Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var settingsRepository: SettingsRepository @Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var localRepository: LocalRepository
private var searchView: SearchView? = null private var searchView: SearchView? = null
private val handler = Handler() private val handler = Handler()
...@@ -108,7 +110,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -108,7 +110,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun showLoading() = view_loading.setVisible(true) override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() {
if (view_loading != null) {
view_loading.setVisible(false)
}
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
showToast(resId) showToast(resId)
...@@ -152,12 +158,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -152,12 +158,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
activity?.apply { activity?.apply {
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(this, recycler_view.addItemDecoration(DividerItemDecoration(this,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start), resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end))) resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter // TODO - use a ViewModel Mapper instead of using settings on the adapter
recycler_view.adapter = ChatRoomsAdapter(this, recycler_view.adapter = ChatRoomsAdapter(
settingsRepository.get(serverInteractor.get()!!)) { chatRoom -> this,
settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom ->
presenter.loadChatRoom(chatRoom) presenter.loadChatRoom(chatRoom)
} }
} }
......
...@@ -22,4 +22,6 @@ interface LocalRepository { ...@@ -22,4 +22,6 @@ interface LocalRepository {
const val SETTINGS_KEY = "settings_" const val SETTINGS_KEY = "settings_"
const val CURRENT_USERNAME_KEY = "username_" const val CURRENT_USERNAME_KEY = "username_"
} }
} }
\ No newline at end of file
fun LocalRepository.checkIfMyself(username: String) = get(LocalRepository.CURRENT_USERNAME_KEY) == username
\ No newline at end of file
...@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_email.textContent = email text_email.textContent = email
text_avatar_url.textContent = "" text_avatar_url.textContent = ""
currentName = username currentName = name
currentUsername = name currentUsername = username
currentEmail = email currentEmail = email
currentAvatar = avatarUrl currentAvatar = avatarUrl
...@@ -76,7 +76,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -76,7 +76,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
} }
override fun hideLoading() { override fun hideLoading() {
view_loading.setVisible(false) if (view_loading != null) {
view_loading.setVisible(false)
}
enableUserInput(true) enableUserInput(true)
} }
......
...@@ -6,5 +6,6 @@ interface CompletionStrategy { ...@@ -6,5 +6,6 @@ interface CompletionStrategy {
fun getItem(prefix: String, position: Int): SuggestionModel fun getItem(prefix: String, position: Int): SuggestionModel
fun autocompleteItems(prefix: String): List<SuggestionModel> fun autocompleteItems(prefix: String): List<SuggestionModel>
fun addAll(list: List<SuggestionModel>) fun addAll(list: List<SuggestionModel>)
fun addPinned(list: List<SuggestionModel>)
fun size(): Int fun size(): Int
} }
\ No newline at end of file
...@@ -2,14 +2,19 @@ package chat.rocket.android.widget.autocompletion.strategy.regex ...@@ -2,14 +2,19 @@ package chat.rocket.android.widget.autocompletion.strategy.regex
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.RESULT_COUNT_UNLIMITED
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
internal class StringMatchingCompletionStrategy(private val threshold: Int = -1) : CompletionStrategy { internal class StringMatchingCompletionStrategy(private val threshold: Int = RESULT_COUNT_UNLIMITED) : CompletionStrategy {
private val list = CopyOnWriteArrayList<SuggestionModel>() private val list = CopyOnWriteArrayList<SuggestionModel>()
private val pinnedList = mutableListOf<SuggestionModel>()
init {
check(threshold >= RESULT_COUNT_UNLIMITED)
}
override fun autocompleteItems(prefix: String): List<SuggestionModel> { override fun autocompleteItems(prefix: String): List<SuggestionModel> {
val result = list.filter { val partialResult = list.filter {
it.searchList.forEach { word -> it.searchList.forEach { word ->
if (word.contains(prefix, ignoreCase = true)) { if (word.contains(prefix, ignoreCase = true)) {
return@filter true return@filter true
...@@ -17,13 +22,23 @@ internal class StringMatchingCompletionStrategy(private val threshold: Int = -1) ...@@ -17,13 +22,23 @@ internal class StringMatchingCompletionStrategy(private val threshold: Int = -1)
} }
false false
}.sortedByDescending { it.pinned } }.sortedByDescending { it.pinned }
return if (threshold == SuggestionsAdapter.UNLIMITED_RESULT_COUNT) result else result.take(threshold) return if (threshold == RESULT_COUNT_UNLIMITED)
partialResult.toList()
else {
val result = partialResult.take(threshold).toMutableList()
result.addAll(pinnedList)
result.toList()
}
} }
override fun addAll(list: List<SuggestionModel>) { override fun addAll(list: List<SuggestionModel>) {
this.list.addAllAbsent(list) this.list.addAllAbsent(list)
} }
override fun addPinned(list: List<SuggestionModel>) {
this.pinnedList.addAll(list)
}
override fun getItem(prefix: String, position: Int): SuggestionModel { override fun getItem(prefix: String, position: Int): SuggestionModel {
return list[position] return list[position]
} }
......
...@@ -27,5 +27,9 @@ class TrieCompletionStrategy : CompletionStrategy { ...@@ -27,5 +27,9 @@ class TrieCompletionStrategy : CompletionStrategy {
} }
} }
override fun addPinned(list: List<SuggestionModel>) {
}
override fun size() = items.size override fun size() = items.size
} }
\ No newline at end of file
...@@ -10,10 +10,10 @@ import kotlin.properties.Delegates ...@@ -10,10 +10,10 @@ 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>() { threshold: Int = MAX_RESULT_COUNT) : RecyclerView.Adapter<VH>() {
companion object { companion object {
// Any number of results. // Any number of results.
const val UNLIMITED_RESULT_COUNT = -1 const val RESULT_COUNT_UNLIMITED = -1
// Trigger suggestions only if on the line start. // Trigger suggestions only if on the line start.
const val CONSTRAINT_BOUND_TO_START = 0 const val CONSTRAINT_BOUND_TO_START = 0
// Trigger suggestions from anywhere. // Trigger suggestions from anywhere.
...@@ -21,12 +21,14 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( ...@@ -21,12 +21,14 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
// Maximum number of results to display by default. // Maximum number of results to display by default.
private const val MAX_RESULT_COUNT = 5 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
// Called to gather results when no results have previously matched. // Called to gather results when no results have previously matched.
private var providerExternal: ((query: String) -> Unit)? = null private var providerExternal: ((query: String) -> Unit)? = null
private var pinnedSuggestions: List<SuggestionModel>? = null
// Maximum number of results/suggestions to display. // Maximum number of results/suggestions to display.
private var resultsThreshold: Int = if (threshold > 0) threshold else UNLIMITED_RESULT_COUNT 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 = StringMatchingCompletionStrategy(resultsThreshold)
// Current input term to look up for suggestions. // Current input term to look up for suggestions.
...@@ -53,6 +55,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( ...@@ -53,6 +55,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
return strategy.autocompleteItems(currentTerm)[position] return strategy.autocompleteItems(currentTerm)[position]
} }
/**
* Set suggestions that should always appear when prompted.
*
* @param suggestions The list of suggestions that will be pinned.
*/
fun setPinnedSuggestions(suggestions: List<SuggestionModel>) {
this.strategy.addPinned(suggestions)
}
fun autocomplete(newTerm: String) { fun autocomplete(newTerm: String) {
this.currentTerm = newTerm.toLowerCase().trim() this.currentTerm = newTerm.toLowerCase().trim()
} }
......
...@@ -23,23 +23,19 @@ import chat.rocket.android.R ...@@ -23,23 +23,19 @@ import chat.rocket.android.R
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
/** // This is a special index that means we're not at an autocompleting state.
* This is a special index that means we're not at an autocompleting state.
*/
private const val NO_STATE_INDEX = 0 private const val NO_STATE_INDEX = 0
class SuggestionsView : FrameLayout, TextWatcher { class SuggestionsView : FrameLayout, TextWatcher {
private val recyclerView: RecyclerView private val recyclerView: RecyclerView
private val registeredTokens = CopyOnWriteArrayList<String>()
// Maps tokens to their respective adapters. // Maps tokens to their respective adapters.
private val adaptersByToken = hashMapOf<String, SuggestionsAdapter<out BaseSuggestionViewHolder>>() private val adaptersByToken = hashMapOf<String, SuggestionsAdapter<out BaseSuggestionViewHolder>>()
private val externalProvidersByToken = hashMapOf<String, ((query: String) -> Unit)>() private val externalProvidersByToken = hashMapOf<String, ((query: String) -> Unit)>()
private val localProvidersByToken = hashMapOf<String, HashMap<String, List<SuggestionModel>>>() private val localProvidersByToken = hashMapOf<String, HashMap<String, List<SuggestionModel>>>()
private var editor: WeakReference<EditText>? = null private var editor: WeakReference<EditText>? = null
private var completionStartIndex = AtomicInteger(NO_STATE_INDEX) private var completionOffset = AtomicInteger(NO_STATE_INDEX)
private var maxHeight: Int = 0 private var maxHeight: Int = 0
companion object { companion object {
...@@ -66,7 +62,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -66,7 +62,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
// If we have a deletion. // If we have a deletion.
if (after == 0) { if (after == 0) {
val deleted = s.subSequence(start, start + count).toString() val deleted = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(deleted) && completionStartIndex.get() > NO_STATE_INDEX) { if (adaptersByToken.containsKey(deleted) && completionOffset.get() > NO_STATE_INDEX) {
// We have removed the '@', '#' or any other action token so halt completion. // We have removed the '@', '#' or any other action token so halt completion.
cancelSuggestions(true) cancelSuggestions(true)
} }
...@@ -77,6 +73,11 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -77,6 +73,11 @@ class SuggestionsView : FrameLayout, TextWatcher {
// If we don't have any adapter bound to any token bail out. // If we don't have any adapter bound to any token bail out.
if (adaptersByToken.isEmpty()) return if (adaptersByToken.isEmpty()) return
if (editor?.get() != null && editor?.get()?.selectionStart ?: 0 <= completionOffset.get()) {
completionOffset.set(NO_STATE_INDEX)
collapse()
}
val new = s.subSequence(start, start + count).toString() val new = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(new)) { if (adaptersByToken.containsKey(new)) {
val constraint = adapter(new).constraint val constraint = adapter(new).constraint
...@@ -84,8 +85,8 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -84,8 +85,8 @@ class SuggestionsView : FrameLayout, TextWatcher {
return return
} }
swapAdapter(getAdapterForToken(new)!!) swapAdapter(getAdapterForToken(new)!!)
completionStartIndex.compareAndSet(NO_STATE_INDEX, start + 1) completionOffset.compareAndSet(NO_STATE_INDEX, start + 1)
editor?.let { this.editor?.let {
// Disable keyboard suggestions when autocompleting. // Disable keyboard suggestions when autocompleting.
val editText = it.get() val editText = it.get()
if (editText != null) { if (editText != null) {
...@@ -97,13 +98,13 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -97,13 +98,13 @@ class SuggestionsView : FrameLayout, TextWatcher {
if (new.startsWith(" ")) { if (new.startsWith(" ")) {
// just halts the completion execution // just halts the completion execution
cancelSuggestions(false) cancelSuggestions(true)
return return
} }
val prefixEndIndex = editor?.get()?.selectionStart ?: NO_STATE_INDEX val prefixEndIndex = this.editor?.get()?.selectionStart ?: NO_STATE_INDEX
if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionStartIndex.get()) return if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionOffset.get()) return
val prefix = s.subSequence(completionStartIndex.get(), editor?.get()?.selectionStart ?: completionStartIndex.get()).toString() val prefix = s.subSequence(completionOffset.get(), this.editor?.get()?.selectionStart ?: completionOffset.get()).toString()
recyclerView.adapter?.let { recyclerView.adapter?.let {
it as SuggestionsAdapter it as SuggestionsAdapter
// we need to look up only after the '@' // we need to look up only after the '@'
...@@ -157,7 +158,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -157,7 +158,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
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 (completionStartIndex.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand() if (completionOffset.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand()
adapter.addItems(list) adapter.addItems(list)
} }
return this return this
...@@ -199,7 +200,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -199,7 +200,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
// Reset completion start index only if we've deleted the token that triggered completion or // Reset completion start index only if we've deleted the token that triggered completion or
// we finished the completion process. // we finished the completion process.
if (haltCompletion) { if (haltCompletion) {
completionStartIndex.set(NO_STATE_INDEX) completionOffset.set(NO_STATE_INDEX)
} }
collapse() collapse()
// Re-enable keyboard suggestions. // Re-enable keyboard suggestions.
...@@ -212,7 +213,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -212,7 +213,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
private fun insertSuggestionOnEditor(item: SuggestionModel) { private fun insertSuggestionOnEditor(item: SuggestionModel) {
editor?.get()?.let { editor?.get()?.let {
val suggestionText = item.text val suggestionText = item.text
it.text.replace(completionStartIndex.get(), it.selectionStart, "$suggestionText ") it.text.replace(completionOffset.get(), it.selectionStart, "$suggestionText ")
} }
} }
......
...@@ -14,15 +14,21 @@ class EmojiParser { ...@@ -14,15 +14,21 @@ class EmojiParser {
*/ */
fun parse(text: CharSequence): CharSequence { fun parse(text: CharSequence): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true) val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannableString = SpannableString.valueOf(unicodedText) var spannable = SpannableString.valueOf(unicodedText)
// Look for groups of emojis, set a CustomTypefaceSpan with the emojione font val typeface = EmojiRepository.cachedTypeface
val length = spannableString.length // Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length
var inEmoji = false var inEmoji = false
var emojiStart = 0 var emojiStart = 0
var offset = 0 var offset = 0
while (offset < length) { while (offset < length) {
val codepoint = unicodedText.codePointAt(offset) val codepoint = unicodedText.codePointAt(offset)
val count = Character.charCount(codepoint) val count = Character.charCount(codepoint)
// Skip control characters.
if (codepoint == 0x2028) {
offset += count
continue
}
if (codepoint >= 0x200) { if (codepoint >= 0x200) {
if (!inEmoji) { if (!inEmoji) {
emojiStart = offset emojiStart = offset
...@@ -30,18 +36,25 @@ class EmojiParser { ...@@ -30,18 +36,25 @@ class EmojiParser {
inEmoji = true inEmoji = true
} else { } else {
if (inEmoji) { if (inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface), spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
inEmoji = false inEmoji = false
} }
offset += count offset += count
if (offset >= length && inEmoji) { if (offset >= length && inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface), spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
return spannableString return spannable
}
private fun calculateSurrogatePairs(scalar: Int): Pair<Int, Int> {
val temp: Int = (scalar - 0x10000) / 0x400
val s1: Int = Math.floor(temp.toDouble()).toInt() + 0xD800
val s2: Int = ((scalar - 0x10000) % 0x400) + 0xDC00
return Pair(s1, s2)
} }
} }
} }
\ No newline at end of file
...@@ -54,6 +54,10 @@ object EmojiRepository { ...@@ -54,6 +54,10 @@ object EmojiRepository {
*/ */
fun getAll() = ALL_EMOJIS fun getAll() = ALL_EMOJIS
// fun findEmojiByUnicode(unicode: Int) {
// ALL_EMOJIS.find { it.unicode == }
// }
/** /**
* Get all emojis for a given category. * Get all emojis for a given category.
* *
...@@ -119,10 +123,7 @@ object EmojiRepository { ...@@ -119,10 +123,7 @@ object EmojiRepository {
var result: String = input.toString() var result: String = input.toString()
while (matcher.find()) { while (matcher.find()) {
val unicode = shortNameToUnicode.get(":${matcher.group(1)}:") val unicode = shortNameToUnicode.get(":${matcher.group(1)}:") ?: continue
if (unicode == null) {
continue
}
if (supported) { if (supported) {
result = result.replace(":" + matcher.group(1) + ":", unicode) result = result.replace(":" + matcher.group(1) + ":", unicode)
...@@ -159,9 +160,7 @@ object EmojiRepository { ...@@ -159,9 +160,7 @@ object EmojiRepository {
private fun buildStringListFromJsonArray(array: JSONArray): List<String> { private fun buildStringListFromJsonArray(array: JSONArray): List<String> {
val list = ArrayList<String>(array.length()) val list = ArrayList<String>(array.length())
for (i in 0..array.length() - 1) { (0 until array.length()).mapTo(list) { array.getString(it) }
list.add(array.getString(i))
}
return list return list
} }
......
...@@ -17,22 +17,22 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac ...@@ -17,22 +17,22 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) { private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int val oldStyle: Int
val old = paint.getTypeface() val old = paint.typeface
if (old == null) { if (old == null) {
oldStyle = 0 oldStyle = 0
} else { } else {
oldStyle = old.getStyle() oldStyle = old.style
} }
val fake = oldStyle and tf.style.inv() val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) { if (fake and Typeface.BOLD != 0) {
paint.setFakeBoldText(true) paint.isFakeBoldText = true
} }
if (fake and Typeface.ITALIC != 0) { if (fake and Typeface.ITALIC != 0) {
paint.setTextSkewX(-0.25f) paint.textSkewX = -0.25f
} }
paint.setTypeface(tf) paint.typeface = tf
} }
} }
\ No newline at end of file
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/darkGray" /> <solid android:color="@color/colorPrimary" />
<size <size
android:width="4dp" android:width="4dp"
android:height="4dp" /> android:height="4dp" />
<corners android:radius="8dp" />
</shape> </shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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: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">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_bar"
app:layout_constraintBottom_toTopOf="@+id/recycler_view_reactions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/top_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintLeft_toRightOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif">
<TextView
android:id="@+id/text_sender"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_message_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM" />
</LinearLayout>
<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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/top_container"
app:layout_constraintTop_toBottomOf="@+id/top_container"
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!" />
<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" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -6,6 +6,17 @@ ...@@ -6,6 +6,17 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/colorPrimary"> android:background="@color/colorPrimary">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:background="@drawable/quote_vertical_bar"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/text_view_action_text" android:id="@+id/text_view_action_text"
android:layout_width="0dp" android:layout_width="0dp"
......
...@@ -6,9 +6,9 @@ ...@@ -6,9 +6,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
android:layout_marginLeft="4dp" android:layout_marginLeft="8dp"
android:layout_marginRight="2dp" android:layout_marginRight="2dp"
android:layout_marginStart="4dp" android:layout_marginStart="8dp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:background="@color/suggestion_background_color"> android:background="@color/suggestion_background_color">
...@@ -22,7 +22,8 @@ ...@@ -22,7 +22,8 @@
android:id="@+id/image_avatar" android:id="@+id/image_avatar"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_margin="4dp" android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:roundedCornerRadius="3dp" app:roundedCornerRadius="3dp"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
......
...@@ -70,6 +70,10 @@ ...@@ -70,6 +70,10 @@
<string name="msg_new_password">Informe a nova senha</string> <string name="msg_new_password">Informe a nova senha</string>
<string name="msg_confirm_password">Confirme a nova senha</string> <string name="msg_confirm_password">Confirme a nova senha</string>
<string name="msg_unread_messages">Mensagens não lidas</string> <string name="msg_unread_messages">Mensagens não lidas</string>
<string name="msg_preview_video">Vídeo</string>
<string name="msg_preview_audio">Audio</string>
<string name="msg_preview_photo">Foto</string>
<string name="msg_no_messages_yet">Nenhuma mensagem ainda</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string> <string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
...@@ -116,6 +120,10 @@ ...@@ -116,6 +120,10 @@
<string name="status_disconnecting">desconectando</string> <string name="status_disconnecting">desconectando</string>
<string name="status_waiting">conectando em %d segundos</string> <string name="status_waiting">conectando em %d segundos</string>
<!--Suggestions-->
<string name="suggest_all_description">Notifica todos nesta sala</string>
<string name="suggest_here_description">Notifica usuários ativos nesta sala</string>
<!-- Slash Commands --> <!-- Slash Commands -->
<string name="Slash_Gimme_Description">Exibir ༼ つ ◕_◕ ༽つ antes de sua mensagem</string> <string name="Slash_Gimme_Description">Exibir ༼ つ ◕_◕ ༽つ antes de sua mensagem</string>
<string name="Slash_LennyFace_Description">Exibir ( ͡° ͜ʖ ͡°) depois de sua mensagem</string> <string name="Slash_LennyFace_Description">Exibir ( ͡° ͜ʖ ͡°) depois de sua mensagem</string>
......
...@@ -71,6 +71,10 @@ ...@@ -71,6 +71,10 @@
<string name="msg_new_password">Enter New Password</string> <string name="msg_new_password">Enter New Password</string>
<string name="msg_confirm_password">Confirm New Password</string> <string name="msg_confirm_password">Confirm New Password</string>
<string name="msg_unread_messages">Unread messages</string> <string name="msg_unread_messages">Unread messages</string>
<string name="msg_preview_video">Video</string>
<string name="msg_preview_audio">Audio</string>
<string name="msg_preview_photo">Photo</string>
<string name="msg_no_messages_yet">No messages yet</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string> <string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
...@@ -117,6 +121,10 @@ ...@@ -117,6 +121,10 @@
<string name="status_disconnecting">disconnecting</string> <string name="status_disconnecting">disconnecting</string>
<string name="status_waiting">connecting in %d seconds</string> <string name="status_waiting">connecting in %d seconds</string>
<!--Suggestions-->
<string name="suggest_all_description">Notify all in this room</string>
<string name="suggest_here_description">Notify active users in this room</string>
<!-- Slash Commands --> <!-- Slash Commands -->
<string name="Slash_Gimme_Description">Displays ༼ つ ◕_◕ ༽つ before your message</string> <string name="Slash_Gimme_Description">Displays ༼ つ ◕_◕ ༽つ before your message</string>
<string name="Slash_LennyFace_Description">Displays ( ͡° ͜ʖ ͡°) after your message</string> <string name="Slash_LennyFace_Description">Displays ( ͡° ͜ʖ ͡°) after your message</string>
......
...@@ -88,6 +88,10 @@ ...@@ -88,6 +88,10 @@
<item name="android:textColor">@color/colorPrimaryText</item> <item name="android:textColor">@color/colorPrimaryText</item>
</style> </style>
<style name="Message.Quote.TextView" parent="Message.TextView">
<item name="android:textColor">@color/colorPrimaryText</item>
</style>
<style name="Timestamp.TextView" parent="TextAppearance.AppCompat.Caption"> <style name="Timestamp.TextView" parent="TextAppearance.AppCompat.Caption">
<item name="android:textSize">10sp</item> <item name="android:textSize">10sp</item>
</style> </style>
......
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