mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 00:29:11 +01:00
Add date headers to CFv2.
This commit is contained in:
@@ -326,6 +326,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
private lateinit var multiselectItemDecoration: MultiselectItemDecoration
|
||||
private lateinit var openableGiftItemDecoration: OpenableGiftItemDecoration
|
||||
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
|
||||
private lateinit var dateHeaderDecoration: DateHeaderDecoration
|
||||
|
||||
private var animationsAllowed = false
|
||||
private var actionMode: ActionMode? = null
|
||||
@@ -464,6 +465,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
adapter.submitList(it) {
|
||||
scrollToPositionDelegate.notifyListCommitted()
|
||||
dateHeaderDecoration.currentItems = it
|
||||
|
||||
if (firstRender) {
|
||||
firstRender = false
|
||||
@@ -785,6 +787,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
binding.conversationDisabledInput.setWallpaperEnabled(wallpaperEnabled)
|
||||
|
||||
adapter.onHasWallpaperChanged(wallpaperEnabled)
|
||||
dateHeaderDecoration.hasWallpaper = wallpaperEnabled
|
||||
}
|
||||
|
||||
private fun presentChatColors(chatColors: ChatColors) {
|
||||
@@ -909,6 +912,9 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
threadHeaderMarginDecoration = ThreadHeaderMarginDecoration()
|
||||
binding.conversationItemRecycler.addItemDecoration(threadHeaderMarginDecoration)
|
||||
|
||||
dateHeaderDecoration = DateHeaderDecoration(hasWallpaper = args.wallpaper != null)
|
||||
binding.conversationItemRecycler.addItemDecoration(dateHeaderDecoration, 0)
|
||||
}
|
||||
|
||||
private fun initializeGiphyMp4(): GiphyMp4ProjectionRecycler {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.toLocalDate
|
||||
import java.util.Locale
|
||||
|
||||
private typealias ConversationElement = MappingModel<*>
|
||||
|
||||
/**
|
||||
* Given the same list as used by the [ConversationAdapterV2], determines where date headers should be rendered
|
||||
* and manages adjusting the list accordingly.
|
||||
*
|
||||
* This is a converted and trimmed down version of [org.thoughtcrime.securesms.util.StickyHeaderDecoration].
|
||||
*/
|
||||
class DateHeaderDecoration(hasWallpaper: Boolean = false, private val scheduleMessageMode: Boolean = false) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val headerCache: MutableMap<Long, DateHeaderViewHolder> = hashMapOf()
|
||||
|
||||
var currentItems: MutableList<ConversationElement?> = mutableListOf()
|
||||
|
||||
var hasWallpaper: Boolean = hasWallpaper
|
||||
set(value) {
|
||||
field = value
|
||||
headerCache.values.forEach { it.updateForWallpaper() }
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
|
||||
val headerHeight = if (position in currentItems.indices && hasHeader(position)) {
|
||||
getHeader(parent, currentItems[position] as ConversationMessageElement).height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
outRect.set(0, headerHeight, 0, 0)
|
||||
}
|
||||
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val count = parent.childCount
|
||||
for (layoutPosition in 0 until count) {
|
||||
val child = parent.getChildAt(parent.childCount - 1 - layoutPosition)
|
||||
val position = parent.getChildAdapterPosition(child)
|
||||
|
||||
if (hasHeader(position)) {
|
||||
val headerView = getHeader(parent, currentItems[position] as ConversationMessageElement).itemView
|
||||
c.save()
|
||||
val left = parent.left
|
||||
val top = child.y.toInt() - headerView.height
|
||||
c.translate(left.toFloat(), top.toFloat())
|
||||
headerView.draw(c)
|
||||
c.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasHeader(position: Int): Boolean {
|
||||
val model = currentItems[position]
|
||||
|
||||
if (model == null || model !is ConversationMessageElement) {
|
||||
return false
|
||||
}
|
||||
|
||||
val previousPosition = position + 1
|
||||
val previousDay: Long
|
||||
if (previousPosition in currentItems.indices) {
|
||||
val previousModel = currentItems[previousPosition]
|
||||
if (previousModel == null || previousModel !is ConversationMessageElement) {
|
||||
return true
|
||||
} else {
|
||||
previousDay = previousModel.toEpochDay()
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
return model.toEpochDay() != previousDay
|
||||
}
|
||||
|
||||
private fun getHeader(parent: RecyclerView, model: ConversationMessageElement): DateHeaderViewHolder {
|
||||
val headerHolder: DateHeaderViewHolder = headerCache.getOrPut(model.toEpochDay()) {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.conversation_item_header, parent, false)
|
||||
val holder = DateHeaderViewHolder(view)
|
||||
holder.bind(model)
|
||||
holder
|
||||
}
|
||||
|
||||
val headerView = headerHolder.itemView
|
||||
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
|
||||
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
|
||||
val childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, headerView.layoutParams.width)
|
||||
val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, headerView.layoutParams.height)
|
||||
headerView.measure(childWidth, childHeight)
|
||||
headerView.layout(0, 0, headerView.measuredWidth, headerView.measuredHeight)
|
||||
return headerHolder
|
||||
}
|
||||
|
||||
private fun ConversationMessageElement.timestamp(): Long {
|
||||
return if (scheduleMessageMode) {
|
||||
(conversationMessage.messageRecord as MediaMmsMessageRecord).scheduledDate
|
||||
} else {
|
||||
conversationMessage.messageRecord.dateSent
|
||||
}
|
||||
}
|
||||
|
||||
private fun ConversationMessageElement.toEpochDay(): Long {
|
||||
return timestamp().toLocalDate().toEpochDay()
|
||||
}
|
||||
|
||||
private inner class DateHeaderViewHolder(val itemView: View) {
|
||||
private val date = itemView.findViewById<TextView>(R.id.text)
|
||||
|
||||
val height: Int
|
||||
get() = itemView.height
|
||||
|
||||
fun bind(model: ConversationMessageElement) {
|
||||
val dateText = DateUtils.getConversationDateHeaderString(itemView.context, Locale.getDefault(), model.timestamp())
|
||||
date.text = dateText
|
||||
updateForWallpaper()
|
||||
}
|
||||
|
||||
fun updateForWallpaper() {
|
||||
if (hasWallpaper) {
|
||||
date.setBackgroundResource(R.drawable.wallpaper_bubble_background_18)
|
||||
date.setTextColor(ContextCompat.getColor(itemView.context, R.color.signal_colorNeutralInverse))
|
||||
} else {
|
||||
date.background = null
|
||||
date.setTextColor(ContextCompat.getColor(itemView.context, R.color.signal_colorOnSurfaceVariant))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.os.Build
|
||||
import android.text.format.DateFormat
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.OffsetDateTime
|
||||
@@ -68,6 +69,13 @@ fun Long.toLocalDateTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalDateTime
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), zoneId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert milliseconds to local date with provided [zoneId].
|
||||
*/
|
||||
fun Long.toLocalDate(zoneId: ZoneId = ZoneId.systemDefault()): LocalDate {
|
||||
return Instant.ofEpochMilli(this).atZone(zoneId).toLocalDate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert milliseconds to local date time with provided [zoneId].
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user