mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-26 14:09:58 +00:00
Add unread divider decoration to CFv2.
This commit is contained in:
committed by
Nicholas Tinsley
parent
2511ca17aa
commit
5731bf023a
@@ -14,7 +14,8 @@ data class ConversationData(
|
||||
val jumpToPosition: Int,
|
||||
val threadSize: Int,
|
||||
val messageRequestData: MessageRequestData,
|
||||
@get:JvmName("showUniversalExpireTimerMessage") val showUniversalExpireTimerMessage: Boolean
|
||||
@get:JvmName("showUniversalExpireTimerMessage") val showUniversalExpireTimerMessage: Boolean,
|
||||
val unreadCount: Int
|
||||
) {
|
||||
|
||||
fun shouldJumpToMessage(): Boolean {
|
||||
|
||||
@@ -119,7 +119,7 @@ public class ConversationRepository {
|
||||
showUniversalExpireTimerUpdate = true;
|
||||
}
|
||||
|
||||
return new ConversationData(conversationRecipient, threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate);
|
||||
return new ConversationData(conversationRecipient, threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate, metadata.getUnreadCount());
|
||||
}
|
||||
|
||||
public void markGiftBadgeRevealed(long messageId) {
|
||||
|
||||
@@ -427,6 +427,7 @@ class ConversationFragment :
|
||||
private lateinit var openableGiftItemDecoration: OpenableGiftItemDecoration
|
||||
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
|
||||
private lateinit var dateHeaderDecoration: DateHeaderDecoration
|
||||
private lateinit var unreadLineDecoration: UnreadLineDecoration
|
||||
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
||||
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
|
||||
|
||||
@@ -563,8 +564,10 @@ class ConversationFragment :
|
||||
|
||||
inputPanel.onPause()
|
||||
|
||||
// todo [cfv2] setLastSeen(System.currentTimeMillis())
|
||||
// todo [cfv2] markLastSeen()
|
||||
unreadLineDecoration.unreadCount = viewModel.unreadCount
|
||||
binding.conversationItemRecycler.invalidateItemDecorations()
|
||||
|
||||
viewModel.markLastSeen()
|
||||
|
||||
motionEventRelay.setDrain(null)
|
||||
EventBus.getDefault().unregister(this)
|
||||
@@ -719,6 +722,7 @@ class ConversationFragment :
|
||||
binding.conversationItemRecycler.height
|
||||
)
|
||||
}
|
||||
unreadLineDecoration.unreadCount = state.meta.unreadCount
|
||||
}
|
||||
.flatMapObservable { it.items.data }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -748,7 +752,7 @@ class ConversationFragment :
|
||||
attachmentManager = AttachmentManager(requireContext(), requireView(), AttachmentManagerListener())
|
||||
|
||||
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
|
||||
viewLifecycleOwner.lifecycle.addObserver(LastSeenPositionUpdater(adapter, layoutManager, viewModel))
|
||||
viewLifecycleOwner.lifecycle.addObserver(LastScrolledPositionUpdater(adapter, layoutManager, viewModel))
|
||||
|
||||
disposables += viewModel.recipient
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -1147,6 +1151,7 @@ class ConversationFragment :
|
||||
|
||||
adapter.onHasWallpaperChanged(wallpaperEnabled)
|
||||
dateHeaderDecoration.hasWallpaper = wallpaperEnabled
|
||||
unreadLineDecoration.hasWallpaper = wallpaperEnabled
|
||||
|
||||
val navColor = if (wallpaperEnabled) {
|
||||
R.color.conversation_navigation_wallpaper
|
||||
@@ -1371,6 +1376,9 @@ class ConversationFragment :
|
||||
|
||||
dateHeaderDecoration = DateHeaderDecoration(hasWallpaper = args.wallpaper != null)
|
||||
binding.conversationItemRecycler.addItemDecoration(dateHeaderDecoration, 0)
|
||||
|
||||
unreadLineDecoration = UnreadLineDecoration(hasWallpaper = args.wallpaper != null)
|
||||
binding.conversationItemRecycler.addItemDecoration(unreadLineDecoration)
|
||||
}
|
||||
|
||||
private fun initializeGiphyMp4(): GiphyMp4ProjectionRecycler {
|
||||
@@ -1631,7 +1639,7 @@ class ConversationFragment :
|
||||
return
|
||||
}
|
||||
|
||||
// todo [cfv2] fragment.setLastSeen(0);
|
||||
unreadLineDecoration.unreadCount = 0
|
||||
|
||||
scrollToPositionDelegate.resetScrollPosition()
|
||||
attachmentManager.cleanup()
|
||||
@@ -3011,7 +3019,7 @@ class ConversationFragment :
|
||||
|
||||
//endregion
|
||||
|
||||
private class LastSeenPositionUpdater(
|
||||
private class LastScrolledPositionUpdater(
|
||||
val adapter: ConversationAdapterV2,
|
||||
val layoutManager: LinearLayoutManager,
|
||||
val viewModel: ConversationViewModel
|
||||
|
||||
@@ -258,7 +258,7 @@ class ConversationRepository(
|
||||
}
|
||||
|
||||
fun setLastVisibleMessageTimestamp(threadId: Long, lastVisibleMessageTimestamp: Long) {
|
||||
SignalExecutors.BOUNDED.submit { SignalDatabase.threads.setLastScrolled(threadId, lastVisibleMessageTimestamp) }
|
||||
SignalExecutors.BOUNDED_IO.execute { SignalDatabase.threads.setLastScrolled(threadId, lastVisibleMessageTimestamp) }
|
||||
}
|
||||
|
||||
fun markGiftBadgeRevealed(messageId: Long) {
|
||||
@@ -575,6 +575,12 @@ class ConversationRepository(
|
||||
}
|
||||
}
|
||||
|
||||
fun markLastSeen(threadId: Long) {
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
SignalDatabase.threads.setLastSeen(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Glide target for a contact photo which expects an error drawable, and publishes
|
||||
* the result to the given emitter.
|
||||
|
||||
@@ -88,6 +88,8 @@ class ConversationViewModel(
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
val showScrollButtonsSnapshot: Boolean
|
||||
get() = scrollButtonStateStore.state.showScrollButtons
|
||||
val unreadCount: Int
|
||||
get() = scrollButtonStateStore.state.unreadCount
|
||||
|
||||
val recipient: Observable<Recipient> = recipientRepository.conversationRecipient
|
||||
|
||||
@@ -395,4 +397,8 @@ class ConversationViewModel(
|
||||
.getScheduledMessageCount(threadId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun markLastSeen() {
|
||||
repository.markLastSeen(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -18,6 +17,8 @@ import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElemen
|
||||
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.drawAsItemDecoration
|
||||
import org.thoughtcrime.securesms.util.layoutIn
|
||||
import org.thoughtcrime.securesms.util.toLocalDate
|
||||
import java.util.Locale
|
||||
|
||||
@@ -61,12 +62,7 @@ class DateHeaderDecoration(hasWallpaper: Boolean = false, private val scheduleMe
|
||||
|
||||
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()
|
||||
headerView.drawAsItemDecoration(c, parent, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,13 +98,8 @@ class DateHeaderDecoration(hasWallpaper: Boolean = false, private val scheduleMe
|
||||
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)
|
||||
headerHolder.itemView.layoutIn(parent)
|
||||
|
||||
return headerHolder
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.drawAsItemDecoration
|
||||
import org.thoughtcrime.securesms.util.layoutIn
|
||||
|
||||
/**
|
||||
* Renders the unread divider in a conversation list based on the unread count.
|
||||
*/
|
||||
class UnreadLineDecoration(hasWallpaper: Boolean) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private var unreadViewHolder: UnreadViewHolder? = null
|
||||
|
||||
var unreadCount: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
unreadViewHolder?.bind()
|
||||
}
|
||||
|
||||
private val firstUnreadPosition: Int
|
||||
get() = unreadCount - 1
|
||||
|
||||
var hasWallpaper: Boolean = hasWallpaper
|
||||
set(value) {
|
||||
field = value
|
||||
unreadViewHolder?.updateForWallpaper()
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (unreadCount == 0) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
return
|
||||
}
|
||||
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
|
||||
val height = if (position == firstUnreadPosition) {
|
||||
getUnreadViewHolder(parent).height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
outRect.set(0, height, 0, 0)
|
||||
}
|
||||
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
for (layoutPosition in 0 until parent.childCount) {
|
||||
val child = parent.getChildAt(layoutPosition)
|
||||
val position = parent.getChildAdapterPosition(child)
|
||||
|
||||
if (position == firstUnreadPosition) {
|
||||
getUnreadViewHolder(parent).itemView.drawAsItemDecoration(c, parent, child)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUnreadViewHolder(parent: RecyclerView): UnreadViewHolder {
|
||||
if (unreadViewHolder != null) {
|
||||
return unreadViewHolder!!
|
||||
}
|
||||
|
||||
unreadViewHolder = UnreadViewHolder(parent)
|
||||
return unreadViewHolder!!
|
||||
}
|
||||
|
||||
private inner class UnreadViewHolder(parent: RecyclerView) {
|
||||
val itemView: View
|
||||
|
||||
private val unreadText: TextView
|
||||
private val unreadDivider: View
|
||||
|
||||
val height: Int
|
||||
get() = itemView.height
|
||||
|
||||
init {
|
||||
itemView = LayoutInflater.from(parent.context).inflate(R.layout.conversation_item_last_seen, parent, false)
|
||||
unreadText = itemView.findViewById(R.id.text)
|
||||
unreadDivider = itemView.findViewById(R.id.last_seen_divider)
|
||||
|
||||
bind()
|
||||
itemView.layoutIn(parent)
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
unreadText.text = itemView.context.resources.getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, unreadCount, unreadCount)
|
||||
updateForWallpaper()
|
||||
}
|
||||
|
||||
fun updateForWallpaper() {
|
||||
if (hasWallpaper) {
|
||||
unreadText.setBackgroundResource(R.drawable.wallpaper_bubble_background_18)
|
||||
unreadDivider.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.transparent_black_80))
|
||||
} else {
|
||||
unreadText.background = null
|
||||
unreadDivider.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.core_grey_45))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1014,16 +1014,13 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
}
|
||||
|
||||
fun setLastSeen(threadId: Long) {
|
||||
setLastSeenSilently(threadId)
|
||||
notifyConversationListListeners()
|
||||
}
|
||||
|
||||
fun setLastSeenSilently(threadId: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(LAST_SEEN to System.currentTimeMillis())
|
||||
.where("$ID = ?", threadId)
|
||||
.run()
|
||||
|
||||
notifyConversationListListeners()
|
||||
}
|
||||
|
||||
fun setLastScrolled(threadId: Long, lastScrolledTimestamp: Long) {
|
||||
@@ -1036,7 +1033,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
|
||||
fun getConversationMetadata(threadId: Long): ConversationMetadata {
|
||||
return readableDatabase
|
||||
.select(LAST_SEEN, HAS_SENT, LAST_SCROLLED)
|
||||
.select(UNREAD_COUNT, LAST_SEEN, HAS_SENT, LAST_SCROLLED)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID = ?", threadId)
|
||||
.run()
|
||||
@@ -1045,13 +1042,15 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
ConversationMetadata(
|
||||
lastSeen = cursor.requireLong(LAST_SEEN),
|
||||
hasSent = cursor.requireBoolean(HAS_SENT),
|
||||
lastScrolled = cursor.requireLong(LAST_SCROLLED)
|
||||
lastScrolled = cursor.requireLong(LAST_SCROLLED),
|
||||
unreadCount = cursor.requireInt(UNREAD_COUNT)
|
||||
)
|
||||
} else {
|
||||
ConversationMetadata(
|
||||
lastSeen = -1L,
|
||||
hasSent = false,
|
||||
lastScrolled = -1
|
||||
lastScrolled = -1,
|
||||
unreadCount = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2033,7 +2032,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
val lastSeen: Long,
|
||||
@get:JvmName("hasSent")
|
||||
val hasSent: Boolean,
|
||||
val lastScrolled: Long
|
||||
val lastScrolled: Long,
|
||||
val unreadCount: Int
|
||||
)
|
||||
|
||||
data class MergeResult(val threadId: Long, val previousThreadId: Long, val neededMerge: Boolean)
|
||||
|
||||
@@ -196,7 +196,7 @@ open class MessageContentProcessorV2(private val context: Context) {
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(destination.id)
|
||||
if (threadId != null) {
|
||||
val (lastSeen) = SignalDatabase.threads.getConversationMetadata(threadId)
|
||||
val lastSeen = SignalDatabase.threads.getConversationMetadata(threadId).lastSeen
|
||||
val visibleThread = ApplicationDependencies.getMessageNotifier().visibleThread.map(ConversationId::threadId).orElse(-1L)
|
||||
|
||||
if (threadId != visibleThread && lastSeen > 0 && lastSeen < pending.receivedTimestamp) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
@@ -71,3 +73,21 @@ fun View.getLifecycle(): Lifecycle? {
|
||||
ViewUtil.getActivityLifecycle(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.layoutIn(parent: View) {
|
||||
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, layoutParams.width)
|
||||
val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, layoutParams.height)
|
||||
measure(childWidth, childHeight)
|
||||
layout(0, 0, measuredWidth, measuredHeight)
|
||||
}
|
||||
|
||||
fun View.drawAsItemDecoration(canvas: Canvas, parent: View, child: View) {
|
||||
canvas.save()
|
||||
val left = parent.left
|
||||
val top = child.y.toInt() - height
|
||||
canvas.translate(left.toFloat(), top.toFloat())
|
||||
draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user