From 5e2d6fc05fdd6c60169604a50613f5e497d47d32 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 19 Jul 2023 16:01:18 -0400 Subject: [PATCH] Fix incorrect unread divider behavior when receiving new messages. --- .../conversation/v2/ConversationFragment.kt | 7 +- .../v2/ConversationItemDecorations.kt | 81 +++++++++++++++++-- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 55b01f065f..02539457ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -579,9 +579,6 @@ class ConversationFragment : inputPanel.onPause() - conversationItemDecorations.unreadCount = viewModel.unreadCount - binding.conversationItemRecycler.invalidateItemDecorations() - viewModel.markLastSeen() motionEventRelay.setDrain(null) @@ -755,7 +752,7 @@ class ConversationFragment : binding.conversationItemRecycler.height ) } - conversationItemDecorations.unreadCount = state.meta.unreadCount + conversationItemDecorations.setFirstUnreadCount(state.meta.unreadCount) } .flatMapObservable { it.items.data } .observeOn(AndroidSchedulers.mainThread()) @@ -1709,8 +1706,6 @@ class ConversationFragment : return } - conversationItemDecorations.unreadCount = 0 - scrollToPositionDelegate.resetScrollPositionAfterMarkListVersionSurpassed() attachmentManager.cleanup() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt index d7af83e26e..7c29afdf81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt @@ -35,16 +35,17 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch private val headerCache: MutableMap = hashMapOf() private var unreadViewHolder: UnreadViewHolder? = null - var unreadCount: Int = 0 + private var unreadState: UnreadState = UnreadState.None set(value) { field = value unreadViewHolder?.bind() } - private val firstUnreadPosition: Int - get() = unreadCount - 1 - - var currentItems: MutableList = mutableListOf() + var currentItems: List = emptyList() + set(value) { + field = value + updateUnreadState(value) + } var hasWallpaper: Boolean = hasWallpaper set(value) { @@ -56,13 +57,13 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val position = parent.getChildAdapterPosition(view) - val unreadHeight = if (unreadCount > 0 && position == firstUnreadPosition) { + val unreadHeight = if (isFirstUnread(position)) { getUnreadViewHolder(parent).height } else { 0 } - val dateHeaderHeight = if (position in currentItems.indices && hasHeader(position)) { + val dateHeaderHeight = if (hasHeader(position)) { getHeader(parent, currentItems[position] as ConversationMessageElement).height } else { 0 @@ -77,7 +78,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch val child = parent.getChildAt(count - 1 - layoutPosition) val position = parent.getChildAdapterPosition(child) - val unreadOffset = if (position == firstUnreadPosition) { + val unreadOffset = if (isFirstUnread(position)) { val unread = getUnreadViewHolder(parent) unread.itemView.drawAsTopItemDecoration(c, parent, child) unread.height @@ -92,6 +93,57 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch } } + /** Must be called before first setting of [currentItems] */ + fun setFirstUnreadCount(unreadCount: Int) { + if (unreadState == UnreadState.None && unreadCount > 0) { + unreadState = UnreadState.InitialUnreadState(unreadCount) + } + } + + /** + * If [unreadState] is [UnreadState.InitialUnreadState] we need to determine the first unread timestamp based on + * initial unread count. + * + * Once in [UnreadState.CompleteUnreadState], need to update the unread count based on new incoming messages since + * the first unread timestamp. If an outgoing message is found in this range the unread state is cleared completely, + * which causes the unread divider to be removed. + */ + private fun updateUnreadState(items: List) { + val state = unreadState + + if (state is UnreadState.InitialUnreadState) { + val timestamp = (items[state.unreadCount - 1] as? ConversationMessageElement)?.timestamp() + if (timestamp != null) { + unreadState = UnreadState.CompleteUnreadState(unreadCount = state.unreadCount, firstUnreadTimestamp = timestamp) + } + } else if (state is UnreadState.CompleteUnreadState) { + var newUnreadCount = 0 + for (element in items) { + if (element is ConversationMessageElement) { + if (element.conversationMessage.messageRecord.isOutgoing) { + unreadState = UnreadState.None + break + } else { + newUnreadCount++ + if (element.timestamp() == state.firstUnreadTimestamp) { + unreadState = state.copy(unreadCount = newUnreadCount) + break + } + } + } + } + } + } + + private fun isFirstUnread(position: Int): Boolean { + val state = unreadState + + return state is UnreadState.CompleteUnreadState && + state.firstUnreadTimestamp != null && + position in currentItems.indices && + (currentItems[position] as? ConversationMessageElement)?.timestamp() == state.firstUnreadTimestamp + } + private fun hasHeader(position: Int): Boolean { val model = if (position in currentItems.indices) { currentItems[position] @@ -134,6 +186,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch private fun getUnreadViewHolder(parent: RecyclerView): UnreadViewHolder { if (unreadViewHolder != null) { + unreadViewHolder!!.itemView.layoutIn(parent) return unreadViewHolder!! } @@ -195,6 +248,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch } fun bind() { + val unreadCount = (unreadState as? UnreadState.CompleteUnreadState)?.unreadCount ?: 0 unreadText.text = itemView.context.resources.getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, unreadCount, unreadCount) updateForWallpaper() } @@ -209,4 +263,15 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch } } } + + sealed class UnreadState { + /** Unread state hasn't been initialized or there are 0 unreads upon entering the conversation */ + object None : UnreadState() + + /** On first load of data, there is at least 1 unread message but we don't know the 'position' in the list yet */ + data class InitialUnreadState(val unreadCount: Int) : UnreadState() + + /** We have at least one unread and know the timestamp of the first unread message and thus 'position' for the header */ + data class CompleteUnreadState(val unreadCount: Int, val firstUnreadTimestamp: Long? = null) : UnreadState() + } }