From e06044acb86fe20281d8c1d6bc8bcd93b7e0d34b Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Tue, 22 Jun 2021 19:28:23 +0200 Subject: [PATCH] Show date in ChatAdapter --- .../main/java/sushi/hardcore/aira/ChatItem.kt | 13 ++ .../hardcore/aira/adapters/ChatAdapter.kt | 141 ++++++++++-------- .../sushi/hardcore/aira/utils/TimeUtils.kt | 5 + app/src/main/res/layout/adapter_chat_file.xml | 46 ------ ...chat_message.xml => adapter_chat_item.xml} | 20 ++- .../main/res/layout/file_bubble_content.xml | 24 +++ .../res/layout/message_bubble_content.xml | 8 + 7 files changed, 142 insertions(+), 115 deletions(-) delete mode 100644 app/src/main/res/layout/adapter_chat_file.xml rename app/src/main/res/layout/{adapter_chat_message.xml => adapter_chat_item.xml} (60%) create mode 100644 app/src/main/res/layout/file_bubble_content.xml create mode 100644 app/src/main/res/layout/message_bubble_content.xml diff --git a/app/src/main/java/sushi/hardcore/aira/ChatItem.kt b/app/src/main/java/sushi/hardcore/aira/ChatItem.kt index 0df9e3c..034c0b9 100644 --- a/app/src/main/java/sushi/hardcore/aira/ChatItem.kt +++ b/app/src/main/java/sushi/hardcore/aira/ChatItem.kt @@ -1,6 +1,7 @@ package sushi.hardcore.aira import sushi.hardcore.aira.background_service.Protocol +import java.util.* class ChatItem(val outgoing: Boolean, val timestamp: Long, val data: ByteArray) { companion object { @@ -14,4 +15,16 @@ class ChatItem(val outgoing: Boolean, val timestamp: Long, val data: ByteArray) } else { if (outgoing) OUTGOING_FILE else INCOMING_FILE } + + val calendar: Calendar by lazy { + Calendar.getInstance().apply { + time = Date(timestamp * 1000) + } + } + val year by lazy { + calendar.get(Calendar.YEAR) + } + val dayOfYear by lazy { + calendar.get(Calendar.DAY_OF_YEAR) + } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt b/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt index 8abbf7f..5e88367 100644 --- a/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt +++ b/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt @@ -19,6 +19,8 @@ import androidx.recyclerview.widget.RecyclerView import sushi.hardcore.aira.ChatItem import sushi.hardcore.aira.R import sushi.hardcore.aira.utils.StringUtils +import sushi.hardcore.aira.utils.TimeUtils +import java.text.DateFormat import java.util.* class ChatAdapter( @@ -29,7 +31,7 @@ class ChatAdapter( companion object { const val BUBBLE_MARGIN = 150 const val CONTAINER_PADDING = 40 - const val BUBBLE_VERTICAL_MARGIN = 40 + const val BUBBLE_VERTICAL_MARGIN = 30 const val BUBBLE_CORNER_NORMAL = 50f const val BUBBLE_CORNER_ARROW = 20f } @@ -48,6 +50,7 @@ class ChatAdapter( fun newLoadedMessage(chatItem: ChatItem) { chatItems.add(0, chatItem) notifyItemInserted(0) + notifyItemChanged(1) } fun clear() { @@ -55,10 +58,20 @@ class ChatAdapter( notifyDataSetChanged() } - internal open class BubbleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { + internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) { private fun generateCorners(topLeft: Float, topRight: Float, bottomRight: Float, bottomLeft: Float): FloatArray { return floatArrayOf(topLeft, topLeft, topRight, topRight, bottomRight, bottomRight, bottomLeft, bottomLeft) } + 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(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) + } + } 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) { @@ -74,10 +87,10 @@ class ChatAdapter( itemView.updatePadding(left = CONTAINER_PADDING) } } - protected fun configureBubble(context: Context, outgoing: Boolean, previousOutgoing: Boolean?, nextOutgoing: Boolean?) { - val bubble = itemView.findViewById(R.id.bubble_content) + protected fun configureBubble(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) { + val bubble = itemView.findViewById(R.id.bubble) bubble.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - gravity = if (outgoing) { + gravity = if (chatItem.outgoing) { marginStart = BUBBLE_MARGIN Gravity.END } else { @@ -86,7 +99,7 @@ class ChatAdapter( } } val backgroundDrawable = GradientDrawable() - backgroundDrawable.setColor(ContextCompat.getColor(context, if (outgoing) { + backgroundDrawable.setColor(ContextCompat.getColor(context, if (chatItem.outgoing) { R.color.bubbleBackground } else { R.color.incomingBubbleBackground @@ -95,36 +108,50 @@ class ChatAdapter( var topRight = BUBBLE_CORNER_NORMAL var bottomRight = BUBBLE_CORNER_NORMAL var bottomLeft = BUBBLE_CORNER_NORMAL - if (nextOutgoing == outgoing) { - if (outgoing) { - bottomRight = BUBBLE_CORNER_ARROW - } else { - bottomLeft = BUBBLE_CORNER_ARROW - } - } - if (previousOutgoing == outgoing) { - if (outgoing) { + if (previousChatItem?.outgoing == chatItem.outgoing && TimeUtils.isInTheSameDay(chatItem, previousChatItem)) { + if (chatItem.outgoing) { topRight = BUBBLE_CORNER_ARROW } else { topLeft = BUBBLE_CORNER_ARROW } } + if (nextChatItem?.outgoing == chatItem.outgoing && TimeUtils.isInTheSameDay(chatItem, nextChatItem)) { + if (chatItem.outgoing) { + bottomRight = BUBBLE_CORNER_ARROW + } else { + bottomLeft = BUBBLE_CORNER_ARROW + } + } backgroundDrawable.cornerRadii = generateCorners(topLeft, topRight, bottomRight, bottomLeft) bubble.background = backgroundDrawable } - protected fun setTimestamp(chatItem: ChatItem): TextView { - val calendar = Calendar.getInstance().apply { - time = Date(chatItem.timestamp * 1000) + protected fun showDateAndTime(chatItem: ChatItem, previousChatItem: ChatItem?) { + val showDate = if (previousChatItem == null) { + true + } else { + !TimeUtils.isInTheSameDay(chatItem, previousChatItem) } - val textView = itemView.findViewById(R.id.timestamp) + val textDate = itemView.findViewById(R.id.text_date) + textDate.visibility = if (showDate) { + textDate.text = DateFormat.getDateInstance().format(chatItem.calendar.time) + View.VISIBLE + } else { + View.GONE + } + val textHour = itemView.findViewById(R.id.text_hour) @SuppressLint("SetTextI18n") - textView.text = StringUtils.toTwoDigits(calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(calendar.get(Calendar.MINUTE)) - return textView + textHour.text = StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.MINUTE)) + textHour.setTextColor(ContextCompat.getColor(context, if (chatItem.outgoing) { + R.color.outgoingTimestamp + } else { + R.color.incomingTimestamp + })) } } - internal open class MessageViewHolder(itemView: View): BubbleViewHolder(itemView) { + internal open class MessageViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) { protected fun bindMessage(chatItem: ChatItem, outgoing: Boolean): TextView { + setBubbleContent(R.layout.message_bubble_content) itemView.findViewById(R.id.text_message).apply { text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString() if (!outgoing) { @@ -135,34 +162,31 @@ class ChatAdapter( } } - internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) { - fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) { - setTimestamp(chatItem).apply { - setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp)) - } - configureBubble(context, true, previousOutgoing, nextOutgoing) + internal class OutgoingMessageViewHolder(context: Context, itemView: View): MessageViewHolder(context, itemView) { + fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) { bindMessage(chatItem, true).apply { setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink)) } - configureContainer(true, previousOutgoing, nextOutgoing == null) + showDateAndTime(chatItem, previousChatItem) + configureBubble(chatItem, previousChatItem, nextChatItem) + configureContainer(true, previousChatItem?.outgoing, nextChatItem == null) } } - internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) { - fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) { - setTimestamp(chatItem).apply { - setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp)) - } - configureBubble(context, false, previousOutgoing, nextOutgoing) + 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)) } - configureContainer(false, previousOutgoing, nextOutgoing == null) + showDateAndTime(chatItem, previousChatItem) + configureBubble(chatItem, previousChatItem, nextChatItem) + configureContainer(false, previousChatItem?.outgoing, nextChatItem == null) } } - internal open class FileViewHolder(itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(itemView) { + internal open class FileViewHolder(context: Context, itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(context, itemView) { protected fun bindFile(chatItem: ChatItem, outgoing: Boolean) { + setBubbleContent(R.layout.file_bubble_content) val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString() itemView.findViewById(R.id.text_filename).apply { text = filename @@ -176,38 +200,33 @@ class ChatAdapter( } } - internal class OutgoingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) { - fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) { - setTimestamp(chatItem).apply { - setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp)) - } + 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) - configureBubble(context, true, previousOutgoing, nextOutgoing) - configureContainer(true, previousOutgoing, nextOutgoing == null) + showDateAndTime(chatItem, previousChatItem) + configureBubble(chatItem, previousChatItem, nextChatItem) + configureContainer(true, previousChatItem?.outgoing, nextChatItem == null) } } - internal class IncomingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) { - fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) { - setTimestamp(chatItem).apply { - setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp)) - } + 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) - configureBubble(context, false, previousOutgoing, nextOutgoing) - configureContainer(false, previousOutgoing, nextOutgoing == null) + showDateAndTime(chatItem, previousChatItem) + configureBubble(chatItem, previousChatItem, nextChatItem) + configureContainer(false, previousChatItem?.outgoing, nextChatItem == null) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = inflater.inflate(R.layout.adapter_chat_item, parent, false) return if (viewType == ChatItem.OUTGOING_MESSAGE || viewType == ChatItem.INCOMING_MESSAGE) { - val view = inflater.inflate(R.layout.adapter_chat_message, parent, false) if (viewType == ChatItem.OUTGOING_MESSAGE) { OutgoingMessageViewHolder(context, view) } else { IncomingMessageViewHolder(context, view) } } else { - val view = inflater.inflate(R.layout.adapter_chat_file, parent, false) if (viewType == ChatItem.OUTGOING_FILE) { OutgoingFileViewHolder(context, view, onSavingFile) } else { @@ -218,21 +237,21 @@ class ChatAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val chatItem = chatItems[position] - val previousOutgoing = if (position == 0) { + val previousChatItem = if (position == 0) { null } else { - chatItems[position-1].outgoing + chatItems[position-1] } - val nextOutgoing = if (position == chatItems.size - 1) { + val nextChatItem = if (position == chatItems.size - 1) { null } else { - chatItems[position+1].outgoing + chatItems[position+1] } when (chatItem.itemType) { - ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem, previousOutgoing, nextOutgoing) - ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem, previousOutgoing, nextOutgoing) - ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem, previousOutgoing, nextOutgoing) - ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem, previousOutgoing, nextOutgoing) + 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) } } diff --git a/app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt b/app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt index 3c54b22..1825040 100644 --- a/app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt +++ b/app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt @@ -1,7 +1,12 @@ package sushi.hardcore.aira.utils +import sushi.hardcore.aira.ChatItem + object TimeUtils { fun getTimestamp(): Long { return System.currentTimeMillis()/1000 } + fun isInTheSameDay(first: ChatItem, second: ChatItem): Boolean { + return first.year == second.year && first.dayOfYear == second.dayOfYear + } } \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_chat_file.xml b/app/src/main/res/layout/adapter_chat_file.xml deleted file mode 100644 index c682681..0000000 --- a/app/src/main/res/layout/adapter_chat_file.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_chat_message.xml b/app/src/main/res/layout/adapter_chat_item.xml similarity index 60% rename from app/src/main/res/layout/adapter_chat_message.xml rename to app/src/main/res/layout/adapter_chat_item.xml index 12a48ce..c897e44 100644 --- a/app/src/main/res/layout/adapter_chat_message.xml +++ b/app/src/main/res/layout/adapter_chat_item.xml @@ -4,23 +4,27 @@ android:layout_height="wrap_content" android:paddingVertical="1dp"> + + - + android:layout_height="wrap_content"/> diff --git a/app/src/main/res/layout/file_bubble_content.xml b/app/src/main/res/layout/file_bubble_content.xml new file mode 100644 index 0000000..924534c --- /dev/null +++ b/app/src/main/res/layout/file_bubble_content.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/message_bubble_content.xml b/app/src/main/res/layout/message_bubble_content.xml new file mode 100644 index 0000000..08da472 --- /dev/null +++ b/app/src/main/res/layout/message_bubble_content.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file