AIRA-android/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt

301 lines
13 KiB
Kotlin
Raw Normal View History

2021-01-26 19:45:18 +01:00
package sushi.hardcore.aira.adapters
2021-06-16 20:57:11 +02:00
import android.annotation.SuppressLint
2021-01-26 19:45:18 +01:00
import android.content.Context
2021-06-22 15:45:31 +02:00
import android.graphics.drawable.GradientDrawable
2021-01-26 19:45:18 +01:00
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.updateMargins
2021-05-05 20:54:25 +02:00
import androidx.core.view.updatePadding
2021-01-26 19:45:18 +01:00
import androidx.recyclerview.widget.RecyclerView
2021-07-31 22:07:32 +02:00
import sushi.hardcore.aira.AIRADatabase
2021-01-26 19:45:18 +01:00
import sushi.hardcore.aira.ChatItem
import sushi.hardcore.aira.R
2021-07-31 22:07:32 +02:00
import sushi.hardcore.aira.background_service.Protocol
2021-06-16 20:57:11 +02:00
import sushi.hardcore.aira.utils.StringUtils
2021-06-22 19:28:23 +02:00
import sushi.hardcore.aira.utils.TimeUtils
import java.text.DateFormat
2021-06-16 20:57:11 +02:00
import java.util.*
2021-01-26 19:45:18 +01:00
class ChatAdapter(
private val context: Context,
private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit
): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val BUBBLE_MARGIN = 150
const val CONTAINER_PADDING = 40
2021-06-22 19:28:23 +02:00
const val BUBBLE_VERTICAL_MARGIN = 30
2021-06-22 15:45:31 +02:00
const val BUBBLE_CORNER_NORMAL = 50f
const val BUBBLE_CORNER_ARROW = 20f
2021-01-26 19:45:18 +01:00
}
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val chatItems = mutableListOf<ChatItem>()
fun newMessage(chatItem: ChatItem) {
chatItems.add(chatItem)
2021-07-31 22:07:32 +02:00
notifyItemChanged(chatItems.size-2)
2021-01-26 19:45:18 +01:00
notifyItemInserted(chatItems.size-1)
}
fun newLoadedMessage(chatItem: ChatItem) {
chatItems.add(0, chatItem)
notifyItemInserted(0)
2021-06-22 19:28:23 +02:00
notifyItemChanged(1)
2021-01-26 19:45:18 +01:00
}
fun removePendingMessages() {
val oldSize = chatItems.size
chatItems.removeAll {
it.timestamp == 0L
}
notifyItemRangeRemoved(chatItems.size, oldSize-chatItems.size)
}
2021-01-26 19:45:18 +01:00
fun clear() {
chatItems.clear()
notifyDataSetChanged()
}
2021-06-22 19:28:23 +02:00
internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) {
2021-06-22 15:45:31 +02:00
private fun generateCorners(topLeft: Float, topRight: Float, bottomRight: Float, bottomLeft: Float): FloatArray {
return floatArrayOf(topLeft, topLeft, topRight, topRight, bottomRight, bottomRight, bottomLeft, bottomLeft)
}
2021-06-22 19:28:23 +02:00
protected fun setBubbleContent(layoutResource: Int) {
//if the view was recycled bubble_content will be null and we don't need to inflate a layout
itemView.findViewById<View>(R.id.bubble_content)?.let { placeHolder ->
val parent = placeHolder.parent as ViewGroup
val index = parent.indexOfChild(placeHolder)
parent.removeView(placeHolder)
val bubbleContent = LayoutInflater.from(context).inflate(layoutResource, parent, false)
parent.addView(bubbleContent, index)
}
}
2021-06-22 15:45:31 +02:00
protected fun configureContainer(outgoing: Boolean, previousOutgoing: Boolean?, isLast: Boolean) {
val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
if (previousOutgoing != null && previousOutgoing != outgoing) {
layoutParams.updateMargins(top = BUBBLE_VERTICAL_MARGIN)
}
2021-06-22 15:45:31 +02:00
if (isLast) {
layoutParams.updateMargins(bottom = BUBBLE_VERTICAL_MARGIN)
}
itemView.layoutParams = layoutParams //set layoutParams anyway to reset margins if the view was recycled
2021-01-26 19:45:18 +01:00
if (outgoing) {
itemView.updatePadding(right = CONTAINER_PADDING)
2021-01-26 19:45:18 +01:00
} else {
itemView.updatePadding(left = CONTAINER_PADDING)
}
}
2021-06-22 19:28:23 +02:00
protected fun configureBubble(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
val bubble = itemView.findViewById<LinearLayout>(R.id.bubble)
2021-06-16 20:57:11 +02:00
bubble.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
2021-06-22 19:28:23 +02:00
gravity = if (chatItem.outgoing) {
marginStart = BUBBLE_MARGIN
Gravity.END
} else {
marginEnd = BUBBLE_MARGIN
Gravity.START
}
}
2021-06-22 15:45:31 +02:00
val backgroundDrawable = GradientDrawable()
2021-06-22 19:28:23 +02:00
backgroundDrawable.setColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
2021-06-22 15:45:31 +02:00
R.color.bubbleBackground
} else {
R.color.incomingBubbleBackground
}))
var topLeft = BUBBLE_CORNER_NORMAL
var topRight = BUBBLE_CORNER_NORMAL
var bottomRight = BUBBLE_CORNER_NORMAL
var bottomLeft = BUBBLE_CORNER_NORMAL
2021-06-22 19:28:23 +02:00
if (previousChatItem?.outgoing == chatItem.outgoing && TimeUtils.isInTheSameDay(chatItem, previousChatItem)) {
if (chatItem.outgoing) {
topRight = BUBBLE_CORNER_ARROW
2021-06-22 15:45:31 +02:00
} else {
2021-06-22 19:28:23 +02:00
topLeft = BUBBLE_CORNER_ARROW
2021-06-22 15:45:31 +02:00
}
2021-01-26 19:45:18 +01:00
}
2021-06-22 19:28:23 +02:00
if (nextChatItem?.outgoing == chatItem.outgoing && TimeUtils.isInTheSameDay(chatItem, nextChatItem)) {
if (chatItem.outgoing) {
bottomRight = BUBBLE_CORNER_ARROW
2021-06-22 15:45:31 +02:00
} else {
2021-06-22 19:28:23 +02:00
bottomLeft = BUBBLE_CORNER_ARROW
2021-06-22 15:45:31 +02:00
}
}
backgroundDrawable.cornerRadii = generateCorners(topLeft, topRight, bottomRight, bottomLeft)
bubble.background = backgroundDrawable
2021-01-26 19:45:18 +01:00
}
2021-06-22 19:28:23 +02:00
protected fun showDateAndTime(chatItem: ChatItem, previousChatItem: ChatItem?) {
2021-07-31 22:07:32 +02:00
var showTextPendingMsg = false
val textPendingMsg = itemView.findViewById<TextView>(R.id.text_pending_msg)
val textDate = itemView.findViewById<TextView>(R.id.text_date)
val textHour = itemView.findViewById<TextView>(R.id.text_hour)
val showDate = if (chatItem.timestamp == 0L) {
if (previousChatItem == null || previousChatItem.timestamp != 0L) {
showTextPendingMsg = true
}
textHour.visibility = View.GONE
false
2021-06-22 19:28:23 +02:00
} else {
2021-07-31 22:07:32 +02:00
textHour.apply {
visibility = View.VISIBLE
@SuppressLint("SetTextI18n")
text = StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.MINUTE))
setTextColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
R.color.outgoingTimestamp
} else {
R.color.incomingTimestamp
}))
}
if (previousChatItem == null) {
true
} else {
!TimeUtils.isInTheSameDay(chatItem, previousChatItem)
}
2021-06-22 19:28:23 +02:00
}
textDate.visibility = if (showDate) {
textDate.text = DateFormat.getDateInstance().format(chatItem.calendar.time)
View.VISIBLE
} else {
View.GONE
2021-06-16 20:57:11 +02:00
}
2021-07-31 22:07:32 +02:00
textPendingMsg.visibility = if (showTextPendingMsg) {
View.VISIBLE
2021-06-22 19:28:23 +02:00
} else {
2021-07-31 22:07:32 +02:00
View.GONE
}
2021-06-16 20:57:11 +02:00
}
2021-01-26 19:45:18 +01:00
}
2021-06-22 19:28:23 +02:00
internal open class MessageViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) {
protected fun bindMessage(chatItem: ChatItem, outgoing: Boolean): TextView {
2021-06-22 19:28:23 +02:00
setBubbleContent(R.layout.message_bubble_content)
2021-01-26 19:45:18 +01:00
itemView.findViewById<TextView>(R.id.text_message).apply {
text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString()
if (!outgoing) {
highlightColor = ContextCompat.getColor(context, R.color.incomingHighlight)
}
return this
2021-01-26 19:45:18 +01:00
}
}
}
2021-06-22 19:28:23 +02:00
internal class OutgoingMessageViewHolder(context: Context, itemView: View): MessageViewHolder(context, itemView) {
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
bindMessage(chatItem, true).apply {
2021-06-16 20:57:11 +02:00
setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink))
}
2021-06-22 19:28:23 +02:00
showDateAndTime(chatItem, previousChatItem)
configureBubble(chatItem, previousChatItem, nextChatItem)
configureContainer(true, previousChatItem?.outgoing, nextChatItem == null)
}
}
2021-06-22 19:28:23 +02:00
internal class IncomingMessageViewHolder(context: Context, itemView: View): MessageViewHolder(context, itemView) {
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
bindMessage(chatItem, false).apply {
setLinkTextColor(ContextCompat.getColor(context, R.color.incomingTextLink))
2021-06-16 20:57:11 +02:00
}
2021-06-22 19:28:23 +02:00
showDateAndTime(chatItem, previousChatItem)
configureBubble(chatItem, previousChatItem, nextChatItem)
configureContainer(false, previousChatItem?.outgoing, nextChatItem == null)
}
}
2021-07-31 22:07:32 +02:00
internal open class FileViewHolder(context: Context, itemView: View, private val onSavingFile: (fileName: String, fileContent: ByteArray) -> Unit): BubbleViewHolder(context, itemView) {
protected fun bindFile(chatItem: ChatItem, outgoing: Boolean) {
2021-06-22 19:28:23 +02:00
setBubbleContent(R.layout.file_bubble_content)
2021-07-31 22:07:32 +02:00
val buttonSave = itemView.findViewById<ImageButton>(R.id.button_save)
val fileName: String
if (chatItem.timestamp == 0L) { //pending
val file = Protocol.parseSmallFile(chatItem.data)!!
fileName = file.rawFileName.decodeToString()
buttonSave.setOnClickListener {
onSavingFile(fileName, file.fileContent)
}
} else {
fileName = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
buttonSave.setOnClickListener {
AIRADatabase.loadFile(chatItem.data.sliceArray(1 until 17))?.let {
onSavingFile(fileName, it)
}
}
}
itemView.findViewById<TextView>(R.id.text_filename).apply {
2021-07-31 22:07:32 +02:00
text = fileName
if (!outgoing) {
highlightColor = ContextCompat.getColor(context, R.color.incomingHighlight)
}
}
}
}
2021-06-22 19:28:23 +02:00
internal class OutgoingFileViewHolder(context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(context, itemView, onSavingFile) {
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
bindFile(chatItem, true)
2021-06-22 19:28:23 +02:00
showDateAndTime(chatItem, previousChatItem)
configureBubble(chatItem, previousChatItem, nextChatItem)
configureContainer(true, previousChatItem?.outgoing, nextChatItem == null)
}
}
2021-06-22 19:28:23 +02:00
internal class IncomingFileViewHolder(context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(context, itemView, onSavingFile) {
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
bindFile(chatItem, false)
2021-06-22 19:28:23 +02:00
showDateAndTime(chatItem, previousChatItem)
configureBubble(chatItem, previousChatItem, nextChatItem)
configureContainer(false, previousChatItem?.outgoing, nextChatItem == null)
2021-01-26 19:45:18 +01:00
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
2021-06-22 19:28:23 +02:00
val view = inflater.inflate(R.layout.adapter_chat_item, parent, false)
return if (viewType == ChatItem.OUTGOING_MESSAGE || viewType == ChatItem.INCOMING_MESSAGE) {
if (viewType == ChatItem.OUTGOING_MESSAGE) {
OutgoingMessageViewHolder(context, view)
} else {
IncomingMessageViewHolder(context, view)
2021-01-26 19:45:18 +01:00
}
} else {
if (viewType == ChatItem.OUTGOING_FILE) {
OutgoingFileViewHolder(context, view, onSavingFile)
} else {
IncomingFileViewHolder(context, view, onSavingFile)
2021-01-26 19:45:18 +01:00
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val chatItem = chatItems[position]
2021-06-22 19:28:23 +02:00
val previousChatItem = if (position == 0) {
null
} else {
2021-06-22 19:28:23 +02:00
chatItems[position-1]
}
2021-06-22 19:28:23 +02:00
val nextChatItem = if (position == chatItems.size - 1) {
2021-06-22 15:45:31 +02:00
null
} else {
2021-06-22 19:28:23 +02:00
chatItems[position+1]
2021-06-22 15:45:31 +02:00
}
2021-01-26 19:45:18 +01:00
when (chatItem.itemType) {
2021-06-22 19:28:23 +02:00
ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem, previousChatItem, nextChatItem)
ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem, previousChatItem, nextChatItem)
ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem, previousChatItem, nextChatItem)
ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem, previousChatItem, nextChatItem)
2021-01-26 19:45:18 +01:00
}
}
override fun getItemCount(): Int {
return chatItems.size
}
override fun getItemViewType(position: Int): Int {
return chatItems[position].itemType
}
}