diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt new file mode 100644 index 00000000..247ed48d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt @@ -0,0 +1,161 @@ +package kr.co.vividnext.sodalive.v2.main.chat.dm.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.extensions.loadUrl +import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatMessageStatus +import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatMessageUiItem + +class DmChatMessageAdapter( + private val onRetryClick: (String) -> Unit +) : RecyclerView.Adapter() { + + private var items: List = emptyList() + + init { + setHasStableIds(true) + } + + fun submitItems(newItems: List) { + val diffResult = DiffUtil.calculateDiff(DmChatMessageDiffCallback(items, newItems)) + items = newItems + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemCount(): Int = items.size + + override fun getItemId(position: Int): Long = stableId(items[position]) + + override fun getItemViewType(position: Int): Int = if (items[position].mine) { + VIEW_TYPE_MY_MESSAGE + } else { + VIEW_TYPE_OPPONENT_MESSAGE + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + VIEW_TYPE_MY_MESSAGE -> MyMessageViewHolder( + inflater.inflate(R.layout.item_dm_chat_my_message, parent, false), + onRetryClick + ) + VIEW_TYPE_OPPONENT_MESSAGE -> OpponentMessageViewHolder( + inflater.inflate(R.layout.item_dm_chat_opponent_message, parent, false) + ) + else -> throw IllegalArgumentException("Unknown viewType: $viewType") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is MyMessageViewHolder -> holder.bind(items[position]) + is OpponentMessageViewHolder -> holder.bind(items[position]) + } + } + + private class DmChatMessageDiffCallback( + private val oldItems: List, + private val newItems: List + ) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldItems.size + override fun getNewListSize(): Int = newItems.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + return sameIdentity(oldItem, newItem) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + oldItems[oldItemPosition] == newItems[newItemPosition] + } + + private class MyMessageViewHolder( + itemView: View, + private val onRetryClick: (String) -> Unit + ) : RecyclerView.ViewHolder(itemView) { + private val tvMessage = itemView.findViewById(R.id.tv_message) + private val tvStatus = itemView.findViewById(R.id.tv_status) + private val ivRetry = itemView.findViewById(R.id.iv_retry) + private val messageContainer = itemView.findViewById(R.id.message_container) + + fun bind(item: DmChatMessageUiItem) { + tvMessage.text = item.textMessage + tvMessage.maxWidth = (itemView.resources.displayMetrics.widthPixels * MESSAGE_MAX_WIDTH_RATIO).toInt() + + val statusText = when (item.status) { + DmChatMessageStatus.SENDING -> itemView.context.getString(R.string.status_sending) + DmChatMessageStatus.FAILED -> itemView.context.getString(R.string.status_failed) + DmChatMessageStatus.SENT -> "" + } + tvStatus.text = statusText + tvStatus.isVisible = statusText.isNotEmpty() + + val showRetry = item.status == DmChatMessageStatus.FAILED && item.localId != null + ivRetry.isVisible = showRetry + if (showRetry) { + ivRetry.setOnClickListener { item.localId?.let(onRetryClick) } + } else { + ivRetry.setOnClickListener(null) + } + + messageContainer.alpha = when (item.status) { + DmChatMessageStatus.SENDING -> 0.6f + DmChatMessageStatus.FAILED -> 0.4f + DmChatMessageStatus.SENT -> 1.0f + } + } + } + + private class OpponentMessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val ivProfile = itemView.findViewById(R.id.iv_profile) + private val tvName = itemView.findViewById(R.id.tv_name) + private val tvMessage = itemView.findViewById(R.id.tv_message) + + fun bind(item: DmChatMessageUiItem) { + tvName.text = item.senderNickname + tvMessage.text = item.textMessage + tvMessage.maxWidth = (itemView.resources.displayMetrics.widthPixels * MESSAGE_MAX_WIDTH_RATIO).toInt() + ivProfile.loadUrl(item.senderProfileImageUrl) { + placeholder(R.drawable.ic_placeholder_profile) + error(R.drawable.ic_placeholder_profile) + transformations(CircleCropTransformation()) + } + } + } + + companion object { + private const val VIEW_TYPE_MY_MESSAGE = 1 + private const val VIEW_TYPE_OPPONENT_MESSAGE = 2 + private const val MESSAGE_MAX_WIDTH_RATIO = 0.68f + + private fun stableId(item: DmChatMessageUiItem): Long = + item.messageId ?: item.localId?.let(::localStableId) ?: localStableId(item.createdAt.toString()) + + private fun localStableId(localId: String): Long { + var hash = 1125899906842597L + localId.forEach { char -> + hash = 31L * hash + char.code + } + return -1L - (hash and Long.MAX_VALUE) + } + + private fun sameIdentity(oldItem: DmChatMessageUiItem, newItem: DmChatMessageUiItem): Boolean { + if (oldItem.messageId != null && newItem.messageId != null) { + return oldItem.messageId == newItem.messageId + } + if (oldItem.localId != null && newItem.localId != null) { + return oldItem.localId == newItem.localId + } + return stableId(oldItem) == stableId(newItem) + } + } +} diff --git a/app/src/main/res/layout/activity_dm_chat_room.xml b/app/src/main/res/layout/activity_dm_chat_room.xml new file mode 100644 index 00000000..50aa648b --- /dev/null +++ b/app/src/main/res/layout/activity_dm_chat_room.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_dm_chat_my_message.xml b/app/src/main/res/layout/item_dm_chat_my_message.xml new file mode 100644 index 00000000..ff2adf60 --- /dev/null +++ b/app/src/main/res/layout/item_dm_chat_my_message.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_dm_chat_opponent_message.xml b/app/src/main/res/layout/item_dm_chat_opponent_message.xml new file mode 100644 index 00000000..8349b210 --- /dev/null +++ b/app/src/main/res/layout/item_dm_chat_opponent_message.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + +