ChatRoomAdapter.kt
package chat.rocket.android.chatroom.adapter
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.ui.bottomsheet.COMPACT_CONFIGURATION
import chat.rocket.android.chatroom.ui.bottomsheet.FULL_CONFIGURATION
import chat.rocket.android.chatroom.ui.bottomsheet.TALL_CONFIGURATION
import chat.rocket.android.chatroom.uimodel.AttachmentUiModel
import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.MessageReplyUiModel
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.chatroom.uimodel.UrlPreviewUiModel
import chat.rocket.android.chatroom.uimodel.toViewType
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomId: String? = null,
private val roomType: String? = null,
private val roomName: String? = null,
private val actionSelectListener: OnActionSelected? = null,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null,
private val navigator: ChatRoomNavigator? = null,
private val analyticsManager: AnalyticsManager? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseUiModel<*>>()
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when (viewType.toViewType()) {
BaseUiModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message)
MessageViewHolder(
view,
actionsListener,
reactionListener,
{
if (roomId != null) {
navigator?.toUserDetails(it, roomId)
}
},
{
if (roomId != null && roomType != null) {
navigator?.toVideoConference(roomId, roomType)
}
}
)
}
BaseUiModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment)
AttachmentViewHolder(
view,
actionsListener,
reactionListener,
actionAttachmentOnClickListener
)
}
BaseUiModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply)
MessageReplyViewHolder(
view,
actionsListener,
reactionListener
) { roomName, permalink ->
actionSelectListener?.openDirectMessage(roomName, permalink)
}
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
}
}
override fun getItemViewType(position: Int): Int = dataSet[position].viewType
override fun getItemCount(): Int = dataSet.size
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
if (holder !is MessageViewHolder) {
if (position + 1 < itemCount) {
val messageAbove = dataSet[position + 1]
if (messageAbove.messageId == dataSet[position].messageId) {
messageAbove.nextDownStreamMessage = dataSet[position]
}
}
} else {
if (position == 0) {
dataSet[0].nextDownStreamMessage = null
} else if (position - 1 > 0) {
if (dataSet[position - 1].messageId != dataSet[position].messageId) {
dataSet[position].nextDownStreamMessage = null
}
}
}
when (holder) {
is MessageViewHolder ->
holder.bind(dataSet[position] as MessageUiModel)
is UrlPreviewViewHolder -> {
holder.bind(dataSet[position] as UrlPreviewUiModel)
}
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel)
is AttachmentViewHolder ->
holder.bind(dataSet[position] as AttachmentUiModel)
}
}
override fun getItemId(position: Int): Long {
val model = dataSet[position]
return when (model) {
is MessageUiModel -> model.messageId.hashCode().toLong()
is AttachmentUiModel -> model.id
else -> return position.toLong()
}
}
fun clearData() {
dataSet.clear()
notifyDataSetChanged()
}
fun appendData(dataSet: List<BaseUiModel<*>>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(dataSet)
notifyItemChanged(previousDataSetSize, dataSet.size)
}
fun prependData(dataSet: List<BaseUiModel<*>>) {
//---At first we will update all already saved elements with received updated ones
val filteredDataSet = dataSet.filter { newItem ->
val matchedIndex =
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType }
if (matchedIndex > -1) {
this.dataSet[matchedIndex] = newItem
notifyItemChanged(matchedIndex)
}
return@filter (matchedIndex < 0)
}
val minAdditionDate = filteredDataSet.minBy { it.message.timestamp } ?: return
//---In the most cases we will just add new elements to the top of messages heap
if (this.dataSet.isEmpty() || minAdditionDate.message.timestamp > this.dataSet[0].message.timestamp) {
this.dataSet.addAll(0, filteredDataSet)
notifyItemRangeInserted(0, filteredDataSet.size)
return
}
//---Else branch: merging messages---
//---We are inserting new received elements into set. Sort them by time+type and show
if (filteredDataSet.isEmpty()) return
this.dataSet.addAll(0, filteredDataSet)
val tmp = this.dataSet.sortedWith(Comparator { t, t2 ->
val timeComparison = t.message.timestamp.compareTo(t2.message.timestamp)
if (timeComparison == 0) {
return@Comparator t.viewType.compareTo(t2.viewType)
}
timeComparison
}).reversed()
this.dataSet.clear()
this.dataSet.addAll(tmp)
notifyDataSetChanged()
}
// FIXME What's 0,1 and 2 means for here?
fun updateItem(message: BaseUiModel<*>): Int {
val index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index")
if (index > -1) {
dataSet[index] = message
dataSet.forEachIndexed { ind, viewModel ->
if (viewModel.messageId == message.messageId) {
if (viewModel.nextDownStreamMessage == null) {
viewModel.reactions = message.reactions
}
if (ind > 0 &&
dataSet[ind].message.timestamp > dataSet[ind - 1].message.timestamp
) {
return 2
} else {
notifyItemChanged(ind)
}
}
}
// Delete message only if current is a system message update, i.e.: Message Removed
if (message.message.isSystemMessage() && indexOfNext > -1 && indexOfNext != index) {
dataSet.removeAt(indexOfNext)
notifyItemRemoved(indexOfNext)
}
return 0
}
return 1
}
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)
}
}
private val actionAttachmentOnClickListener = object : ActionAttachmentOnClickListener {
override fun onActionClicked(view: View, action: Action) {
val temp = action as ButtonAction
if (temp.url != null && temp.isWebView != null) {
if (temp.isWebView == true) {
//Open in a configurable sizable WebView
when (temp.webViewHeightRatio) {
FULL_CONFIGURATION -> openFullWebPage(temp, roomId)
COMPACT_CONFIGURATION -> openConfigurableWebPage(
temp,
roomId,
FULL_CONFIGURATION
)
TALL_CONFIGURATION -> openConfigurableWebPage(
temp,
roomId,
TALL_CONFIGURATION
)
else -> Unit
}
Timber.d("Open in a configurable sizable webview")
} else {
//Open in chrome custom tab
temp.url?.let { view.openTabbedUrl(it) }
}
} else if (temp.message != null && temp.isMessageInChatWindow != null) {
if (temp.isMessageInChatWindow == true) {
//Send to chat window
temp.message?.let {
if (roomId != null) {
actionSelectListener?.sendMessage(roomId, it)
}
}
} else {
//TODO: Send to bot but not in chat window
Timber.d("Send to bot but not in chat window")
}
}
}
private fun openConfigurableWebPage(
temp: ButtonAction,
roomId: String?,
heightRatio: String
) {
temp.url?.let {
if (roomId != null) {
actionSelectListener?.openConfigurableWebPage(roomId, it, heightRatio)
}
}
}
private fun openFullWebPage(temp: ButtonAction, roomId: String?) {
temp.url?.let {
if (roomId != null) {
actionSelectListener?.openFullWebPage(roomId, it)
}
}
}
}
private val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) {
if (analyticsManager != null && roomName != null && roomType != null && actionSelectListener != null) {
with(message) {
when (item.itemId) {
R.id.action_info -> {
actionSelectListener.showMessageInfo(id)
analyticsManager.logMessageActionInfo()
}
R.id.action_reply -> {
actionSelectListener.citeMessage(roomName, roomType, id, true)
analyticsManager.logMessageActionReply()
}
R.id.action_quote -> {
actionSelectListener.citeMessage(roomName, roomType, id, false)
analyticsManager.logMessageActionQuote()
}
R.id.action_copy -> {
actionSelectListener.copyMessage(id)
analyticsManager.logMessageActionCopy()
}
R.id.action_edit -> {
actionSelectListener.editMessage(roomId, id, this.message)
analyticsManager.logMessageActionEdit()
}
R.id.action_star -> {
actionSelectListener.toggleStar(id, !item.isChecked)
analyticsManager.logMessageActionStar()
}
R.id.action_pin -> {
actionSelectListener.togglePin(id, !item.isChecked)
analyticsManager.logMessageActionPin()
}
R.id.action_delete -> {
actionSelectListener.deleteMessage(roomId, id)
analyticsManager.logMessageActionDelete()
}
R.id.action_add_reaction -> {
actionSelectListener.showReactions(id)
analyticsManager.logMessageActionAddReaction()
}
R.id.action_permalink -> {
actionSelectListener.copyPermalink(id)
analyticsManager.logMessageActionPermalink()
}
R.id.action_report -> {
actionSelectListener.reportMessage(id)
analyticsManager.logMessageActionReport()
}
}
}
}
}
}
interface OnActionSelected {
fun showMessageInfo(id: String)
fun citeMessage(
roomName: String,
roomType: String,
messageId: String,
mentionAuthor: Boolean
)
fun copyMessage(id: String)
fun editMessage(roomId: String, messageId: String, text: String)
fun toggleStar(id: String, star: Boolean)
fun togglePin(id: String, pin: Boolean)
fun deleteMessage(roomId: String, id: String)
fun showReactions(id: String)
fun openDirectMessage(roomName: String, message: String)
fun sendMessage(chatRoomId: String, text: String)
fun copyPermalink(id: String)
fun reportMessage(id: String)
fun openFullWebPage(roomId: String, url: String)
fun openConfigurableWebPage(roomId: String, url: String, heightRatio: String)
}
}