Commit a244456a authored by Lucio Maciel's avatar Lucio Maciel

URL preview, multiple view types on the messages list

parent b3e18e41
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible
import kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View) : BaseViewHolder<AudioAttachmentViewModel>(itemView) {
init {
with(itemView) {
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
}
}
override fun bindViews(data: AudioAttachmentViewModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
abstract class BaseViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView) {
var data: T? = null
fun bind(data: T) {
this.data = data
bindViews(data)
}
abstract fun bindViews(data: T)
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.viewmodel.*
import chat.rocket.android.util.extensions.inflate
import timber.log.Timber
import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>()
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when(viewType.toViewType()) {
BaseViewModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, roomName, roomType, presenter, enableActions)
}
BaseViewModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view)
}
BaseViewModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view)
}
BaseViewModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view)
}
BaseViewModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
}
}
override fun getItemViewType(position: Int): Int {
return dataSet[position].viewType
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
when (holder) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
}
}
override fun getItemId(position: Int): Long {
val model = dataSet[position]
return when (model) {
is MessageViewModel -> model.messageId.hashCode().toLong()
is BaseFileAttachmentViewModel -> model.id
else -> return position.toLong()
}
}
fun appendData(dataSet: List<BaseViewModel<*>>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(dataSet)
notifyItemChanged(previousDataSetSize, dataSet.size)
}
fun prependData(dataSet: List<BaseViewModel<*>>) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
}
fun updateItem(message: BaseViewModel<*>) {
val index = dataSet.indexOfLast { it.messageId == message.messageId }
Timber.d("index: $index")
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.messageId == messageId }
if (index > -1) {
val oldSize = dataSet.size
val newSet = dataSet.filterNot { it.messageId == messageId }
dataSet.clear()
dataSet.addAll(newSet)
val newSize = dataSet.size
notifyItemRangeRemoved(index, oldSize - newSize)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(itemView: View) : BaseViewHolder<ImageAttachmentViewModel>(itemView) {
override fun bindViews(data: ImageAttachmentViewModel) {
with(itemView) {
image_attachment.setImageURI(data.attachmentUrl)
file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setStartPosition(0)
.show()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class MessageViewHolder(
itemView: View,
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
enableActions: Boolean
) : BaseViewHolder<MessageViewModel>(itemView),
MenuItem.OnMenuItemClickListener {
init {
itemView.text_content.movementMethod = LinkMovementMethod()
if (enableActions) {
itemView.setOnLongClickListener {
if (data?.isSystemMessage == false) {
val menuItems = it.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = data?.isPinned ?: false
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned
}
val adapter = ActionListAdapter(menuItems, this@MessageViewHolder)
BottomSheetMenu(adapter).apply {
}.show(it.context)
}
true
}
}
}
override fun bindViews(data: MessageViewModel) {
with(itemView) {
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
image_avatar.setImageURI(data.avatar)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
data?.rawData?.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id)
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message)
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
return true
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.util.extensions.content
import kotlinx.android.synthetic.main.message_url_preview.view.*
class UrlPreviewViewHolder(itemView: View) : BaseViewHolder<UrlPreviewViewModel>(itemView) {
override fun bindViews(data: UrlPreviewViewModel) {
with(itemView) {
image_preview.setImageURI(data.thumbUrl)
text_host.content = data.hostname
text_title.content = data.title
text_description.content = data.description
url_preview_layout.setOnClickListener { view ->
view.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.rawData.url)))
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible
import kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View) : BaseViewHolder<VideoAttachmentViewModel>(itemView) {
init {
with(itemView) {
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
}
}
override fun bindViews(data: VideoAttachmentViewModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import android.net.Uri
import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
......@@ -34,7 +35,8 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
factory: RocketChatClientFactory,
private val mapper: MessageViewModelMapper) {
private val mapper: ViewModelMapper,
private val oldMapper: MessageViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!)
private var subId: String? = null
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
......@@ -48,7 +50,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
messagesRepository.saveAll(messages)
val messagesViewModels = mapper.mapToViewModelList(messages, settings)
val messagesViewModels = mapper.map(messages)
view.showMessages(messagesViewModels)
// Subscribe after getting the first page of messages from REST
......@@ -319,7 +321,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.mapToViewModel(streamedMessage, settings)
val viewModelStreamedMessage = mapper.map(streamedMessage)
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) {
......
package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
......@@ -12,7 +12,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param dataSet The data set to show.
*/
fun showMessages(dataSet: List<MessageViewModel>)
fun showMessages(dataSet: List<BaseViewModel<*>>)
/**
* Send a message to a chat room.
......@@ -43,7 +43,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param message The (recent) message sent to a chat room.
*/
fun showNewMessage(message: MessageViewModel)
fun showNewMessage(message: List<BaseViewModel<*>>)
/**
* Dispatch to the recycler views adapter that we should remove a message.
......@@ -57,7 +57,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param index The index of the changed message
*/
fun dispatchUpdateMessage(index: Int, message: MessageViewModel)
fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>)
/**
* Show reply status above the message composer.
......
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
......@@ -18,7 +19,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: MessageViewModelMapper,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor) {
......@@ -41,8 +42,8 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
val pinnedMessages =
client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.mapToViewModelList(pinnedMessages.result, settings)
.filterNot { it.isSystemMessage }
val messageList = mapper.map(pinnedMessages.result)
.filter { it is MessageViewModel}.filterNot { (it as MessageViewModel).isSystemMessage }
view.showPinnedMessages(messageList)
view.hideLoading()
}.ifNull {
......
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
......@@ -11,5 +11,5 @@ interface PinnedMessagesView : MessageView, LoadingView {
*
* @param pinnedMessages The list of pinned messages.
*/
fun showPinnedMessages(pinnedMessages: List<MessageViewModel>)
fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>)
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class ChatRoomAdapter(private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter) : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
private val dataSet = ArrayList<MessageViewModel>()
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message), roomType, roomName, presenter)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
override fun getItemId(position: Int): Long = dataSet[position].id.hashCode().toLong()
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
fun addItem(message: MessageViewModel) {
dataSet.add(0, message)
notifyItemInserted(0)
}
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
class ViewHolder(itemView: View,
val roomType: String,
val roomName: String,
val presenter: ChatRoomPresenter) : RecyclerView.ViewHolder(itemView), MenuItem.OnMenuItemClickListener {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel) = with(itemView) {
messageViewModel = message
image_avatar.setImageURI(message.avatarUri)
text_sender.text = message.senderName
text_message_time.content = message.time
text_content.content = message.content
text_content.movementMethod = LinkMovementMethod()
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment, file_name)
text_content.setOnClickListener {
if (!message.isSystemMessage) {
val menuItems = it.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = message.isPinned
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
setChecked(isPinned)
}
val adapter = ActionListAdapter(menuItems, this@ViewHolder)
presenter.dispatchRestoreUIState()
BottomSheetMenu(adapter).show(it.context)
}
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
messageViewModel.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter.copyMessage(id)
R.id.action_menu_msg_edit -> presenter.editMessage(roomId, id, getOriginalMessage())
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter.pinMessage(id)
} else {
presenter.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
return true
}
private fun bindAttachment(message: MessageViewModel,
attachment_container: View,
image_attachment: SimpleDraweeView,
audio_video_attachment: View,
file_name: TextView) {
with(message) {
if (attachmentUrl == null || attachmentType == null) {
attachment_container.setVisible(false)
return
}
var imageVisible = false
var videoVisible = false
attachment_container.setVisible(true)
when (message.attachmentType) {
is AttachmentType.Image -> {
imageVisible = true
image_attachment.setImageURI(message.attachmentUrl)
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(message.attachmentUrl))
.setStartPosition(0)
.show()
}
}
is AttachmentType.Video,
is AttachmentType.Audio -> {
videoVisible = true
audio_video_attachment.setOnClickListener { view ->
message.attachmentUrl?.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
image_attachment.setVisible(imageVisible)
audio_video_attachment.setVisible(videoVisible)
file_name.text = message.attachmentTitle
}
}
}
}
\ No newline at end of file
......@@ -15,9 +15,10 @@ import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.*
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
......@@ -144,7 +145,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.EmojiKeyboardLi
return true
}
override fun showMessages(dataSet: List<MessageViewModel>) {
override fun showMessages(dataSet: List<BaseViewModel<*>>) {
activity?.apply {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
......@@ -161,9 +162,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.EmojiKeyboardLi
})
}
}
val oldMessagesCount = adapter.itemCount
adapter.addDataSet(dataSet)
if (oldMessagesCount == 0 && dataSet.size > 0) {
adapter.appendData(dataSet)
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0)
}
}
......@@ -182,8 +184,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.EmojiKeyboardLi
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file))
override fun showNewMessage(message: MessageViewModel) {
adapter.addItem(message)
override fun showNewMessage(message: List<BaseViewModel<*>>) {
adapter.prependData(message)
recycler_view.scrollToPosition(0)
}
......@@ -204,8 +206,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.EmojiKeyboardLi
actionSnackbar.dismiss()
}
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) {
adapter.updateItem(message)
override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) {
adapter.updateItem(message.last())
}
override fun dispatchDeleteMessage(msgId: String) {
......
package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
class PinnedMessagesAdapter : RecyclerView.Adapter<PinnedMessagesAdapter.ViewHolder>() {
init {
setHasStableIds(true)
}
val dataSet = ArrayList<MessageViewModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message))
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
onBindViewHolder(holder, position)
}
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
fun addItem(message: MessageViewModel) {
dataSet.add(0, message)
notifyItemInserted(0)
}
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.hashCode().toLong()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel) = with(itemView) {
messageViewModel = message
image_avatar.setImageURI(message.avatarUri)
text_sender.content = message.senderName
text_message_time.content = message.time
text_content.content = message.content
text_content.movementMethod = LinkMovementMethod()
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment,
file_name)
}
private fun bindAttachment(message: MessageViewModel,
attachment_container: View,
image_attachment: SimpleDraweeView,
audio_video_attachment: View,
file_name: TextView) {
with(message) {
if (attachmentUrl == null || attachmentType == null) {
attachment_container.setVisible(false)
return
}
var imageVisible = false
var videoVisible = false
attachment_container.setVisible(true)
when (message.attachmentType) {
is AttachmentType.Image -> {
imageVisible = true
image_attachment.setImageURI(message.attachmentUrl)
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(message.attachmentUrl))
.setStartPosition(0)
.show()
}
}
is AttachmentType.Video,
is AttachmentType.Audio -> {
videoVisible = true
audio_video_attachment.setOnClickListener { view ->
message.attachmentUrl?.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
image_attachment.setVisible(imageVisible)
audio_video_attachment.setVisible(videoVisible)
file_name.text = message.attachmentTitle
}
}
}
}
\ No newline at end of file
......@@ -9,9 +9,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.presentation.PinnedMessagesPresenter
import chat.rocket.android.chatroom.presentation.PinnedMessagesView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
......@@ -39,7 +40,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var adapter: PinnedMessagesAdapter
private lateinit var adapter: ChatRoomAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -71,10 +72,11 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<MessageViewModel>) {
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
activity?.apply {
if (recycler_view_pinned.adapter == null) {
adapter = PinnedMessagesAdapter()
// TODO - add a better constructor for this case...
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, null, false)
recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager
......@@ -88,7 +90,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
}
}
adapter.addDataSet(pinnedMessages)
adapter.appendData(pinnedMessages)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.attachment.AudioAttachment
data class AudioAttachmentViewModel(
override val rawData: AudioAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
interface BaseFileAttachmentViewModel<out T> : BaseViewModel<T> {
val attachmentUrl: String
val attachmentTitle: CharSequence
val id: Long
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
interface BaseMessageViewModel<out T> : BaseViewModel<T> {
val avatar: String
val time: CharSequence
val senderName: CharSequence
val content: CharSequence
val isPinned: Boolean
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import java.security.InvalidParameterException
interface BaseViewModel<out T> {
val rawData: T
val messageId: String
val viewType: Int
val layoutId: Int
enum class ViewType(val viewType: Int) {
MESSAGE(0),
SYSTEM_MESSAGE(1),
URL_PREVIEW(2),
IMAGE_ATTACHMENT(3),
VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6)
}
}
internal fun Int.toViewType(): BaseViewModel.ViewType {
return BaseViewModel.ViewType.values().firstOrNull { it.viewType == this }
?: throw InvalidParameterException("Invalid viewType: $this for BaseViewModel.ViewType")
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.attachment.ImageAttachment
data class ImageAttachmentViewModel(
override val rawData: ImageAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import android.content.Context
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class MessageViewModelMapper @Inject constructor(private val context: Context,
private val tokenRepository: TokenRepository,
private val messageParser: MessageParser,
private val messagesRepository: MessagesRepository,
private val localRepository: LocalRepository,
private val currentServerRepository: CurrentServerRepository) {
suspend fun mapToViewModel(message: Message, settings: Map<String, Value<Any>>): MessageViewModel = withContext(CommonPool) {
MessageViewModel(
this@MessageViewModelMapper.context,
tokenRepository.get(),
message,
settings,
messageParser,
messagesRepository,
localRepository,
currentServerRepository
)
}
suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>): List<MessageViewModel> {
return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings,
messageParser, messagesRepository, localRepository, currentServerRepository) }
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.url.Url
data class UrlPreviewViewModel(
override val rawData: Url,
override val messageId: String,
val title: CharSequence?,
val hostname: String,
val description: CharSequence?,
val thumbUrl: String?
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
override val layoutId: Int
get() = R.layout.message_url_preview
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.attachment.VideoAttachment
data class VideoAttachmentViewModel(
override val rawData: VideoAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
......@@ -2,7 +2,7 @@ package chat.rocket.android.chatrooms.ui
import DateTimeHelper
import android.content.Context
import android.support.v4.content.res.ResourcesCompat
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
......@@ -28,8 +28,6 @@ class ChatRoomsAdapter(private val context: Context,
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun updateRooms(newRooms: List<ChatRoom>) {
dataSet.clear()
dataSet.addAll(newRooms)
......@@ -45,16 +43,27 @@ class ChatRoomsAdapter(private val context: Context,
bindUnreadMessages(chatRoom, text_total_unread_messages)
if (chatRoom.alert || chatRoom.unread > 0) {
text_chat_name.alpha = 1F
text_last_message_date_time.setTextColor(ResourcesCompat.getColor(resources, R.color.colorAccent, null))
text_last_message.setTextColor(ResourcesCompat.getColor(resources, android.R.color.primary_text_light, null))
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorAccent))
text_last_message.setTextColor(ContextCompat.getColor(context,
android.R.color.primary_text_light))
} else {
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorPrimaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
}
setOnClickListener { listener(chatRoom) }
}
private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) {
drawee.setImageURI(UrlHelper.getAvatarUrl(chatRoom.client.url, chatRoom.name))
val avatarId = /*if (chatRoom.type is RoomType.DirectMessage) chatRoom.name else chatRoom.id*/ chatRoom.name
drawee.setImageURI(UrlHelper.getAvatarUrl(chatRoom.client.url, avatarId))
}
private fun bindName(chatRoom: ChatRoom, textView: TextView) {
......@@ -66,6 +75,8 @@ class ChatRoomsAdapter(private val context: Context,
if (lastMessage != null) {
val localDateTime = DateTimeHelper.getLocalDateTime(lastMessage.timestamp)
textView.textContent = DateTimeHelper.getDate(localDateTime, context)
} else {
textView.textContent = ""
}
}
......@@ -101,6 +112,7 @@ class ChatRoomsAdapter(private val context: Context,
textView.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
textView.setVisible(true)
}
else -> textView.setVisible(false)
}
}
}
......
......@@ -39,6 +39,7 @@ import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
......@@ -101,6 +102,9 @@ class AppModule {
fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient {
return OkHttpClient.Builder().apply {
addInterceptor(logger)
connectTimeout(15, TimeUnit.SECONDS)
readTimeout(20, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
}.build()
}
......
......@@ -63,7 +63,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
var quoteNode = parser.parse("> $senderName $time")
parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.getOriginalMessage())}")
quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
}
......
......@@ -82,4 +82,6 @@ fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> {
fun Map<String, Value<Any>>.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
}
\ No newline at end of file
}
fun Map<String, Value<Any>>.baseUrl() : String? = this[SITE_URL]?.value as String
\ No newline at end of file
......@@ -43,7 +43,7 @@ var TextView.hintContent: String
hint = value
}
var TextView.content: CharSequence
var TextView.content: CharSequence?
get() = text
set(value) {
Markwon.unscheduleDrawables(this)
......
......@@ -31,7 +31,6 @@
style="@style/ChatRoom.Name.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:alpha="0.6"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/text_last_message_date_time"
app:layout_constraintTop_toTopOf="parent"
......
......@@ -48,20 +48,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="2dp"
app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent"
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!" />
<!-- TODO - Use separate adapter items for messages and attachments. -->
<include
android:id="@+id/message_attachment"
layout="@layout/message_attachment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_content" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -5,16 +5,17 @@
android:id="@+id/attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:orientation="vertical">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
android:layout_width="match_parent"
android:layout_height="150dp"
android:visibility="gone"
fresco:actualImageScaleType="fitStart"
fresco:placeholderImage="@drawable/image_dummy"
tools:visibility="visible" />
android:visibility="visible"
fresco:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/image_dummy" />
<FrameLayout
android:id="@+id/audio_video_attachment"
......@@ -22,7 +23,7 @@
android:layout_height="150dp"
android:background="@color/black"
android:visibility="gone"
tools:visibility="gone">
tools:visibility="visible">
<ImageView
android:layout_width="wrap_content"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/url_preview_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginEnd="24dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_preview"
android:layout_width="70dp"
android:layout_height="50dp"
app:actualImageScaleType="centerCrop"/>
<TextView
android:id="@+id/text_host"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorSecondaryText"
tools:text="www.uol.com.br"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_preview" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
tools:text="Web page title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_host"/>
<TextView
android:id="@+id/text_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_title"/>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -9,6 +9,7 @@
<!-- Text colors -->
<color name="colorPrimaryText">#DE000000</color>
<color name="colorSecondaryText">#787878</color>
<!-- User status colors -->
<color name="colorUserStatusOnline">#2FE1A8</color>
......
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