Updated the conversation to highlight the first unread message on open

This commit is contained in:
Morgan Pretty 2023-06-09 18:05:17 +10:00
parent 3bd2883707
commit da02d385d1
5 changed files with 129 additions and 29 deletions

View File

@ -176,6 +176,7 @@ import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
@ -341,6 +342,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val messageToScrollTimestamp = AtomicLong(-1)
private val messageToScrollAuthor = AtomicReference<Address?>(null)
private val firstLoad = AtomicBoolean(true)
private val forceHighlightNextLoad = AtomicInteger(-1)
private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1
@ -419,15 +421,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val weakActivity = WeakReference(this)
lifecycleScope.launch(Dispatchers.IO) {
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
// Note: We are accessing the `adapter` property because we want it to be loaded on
// the background thread to avoid blocking the UI thread and potentially hanging when
// transitioning to the activity
weakActivity.get()?.adapter ?: return@launch
withContext(Dispatchers.Main) {
updateUnreadCountIndicator()
setUpRecyclerView()
setUpTypingObserver()
setUpRecipientObserver()
@ -503,19 +502,41 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val messageTimestamp = messageToScrollTimestamp.getAndSet(-1)
val author = messageToScrollAuthor.getAndSet(null)
// Update the unreadCount value to be loaded from the database since we got a new message
if (firstLoad.get() || oldCount != newCount) {
// Update the unreadCount value to be loaded from the database since we got a new message
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
updateUnreadCountIndicator()
}
if (author != null && messageTimestamp >= 0) {
jumpToMessage(author, messageTimestamp, null)
}
else if (firstLoad.getAndSet(false)) {
scrollToFirstUnreadMessageIfNeeded(true)
// We can't actually just 'shouldHighlight = true' here because any unread messages will
// immediately be marked as ready triggering a reload of the cursor
val lastSeenItemPosition = scrollToFirstUnreadMessageIfNeeded(true)
handleRecyclerViewScrolled()
if (lastSeenItemPosition != null) {
forceHighlightNextLoad.set(lastSeenItemPosition)
}
}
else if (oldCount != newCount) {
// Update the unreadCount value to be loaded from the database since we got a new message
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
updateUnreadCountIndicator()
handleRecyclerViewScrolled()
}
else {
// Really annoying but if a message gets marked as read during the initial load it'll
// immediately result in a subsequent load of the cursor, if we trigger the highlight
// within the 'firstLoad' it generally ends up getting repositioned as the views get
// recycled and the wrong view is highlighted - by doing it on the subsequent load the
// correct view is highlighted
val forceHighlightPosition = forceHighlightNextLoad.getAndSet(-1)
if (forceHighlightPosition != -1) {
highlightViewAtPosition(forceHighlightPosition)
}
}
}
updatePlaceholder()
}
@ -716,19 +737,29 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
private fun scrollToFirstUnreadMessageIfNeeded(isFirstLoad: Boolean = false) {
private fun scrollToFirstUnreadMessageIfNeeded(isFirstLoad: Boolean = false, shouldHighlight: Boolean = false): Int? {
val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(viewModel.threadId).first()
val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return -1
// If this is triggered when first opening a conversation then we want to position the top
// of the first unread message in the middle of the screen
if (isFirstLoad && !reverseMessageList) {
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
return
if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
return lastSeenItemPosition
}
if (lastSeenItemPosition <= 3) { return }
if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
return lastSeenItemPosition
}
private fun highlightViewAtPosition(position: Int) {
binding?.conversationRecyclerView?.post {
(layoutManager?.findViewByPosition(position) as? VisibleMessageView)?.playHighlight()
}
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
@ -15,9 +14,7 @@ import android.view.View
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
import androidx.core.view.children
@ -28,6 +25,7 @@ import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
@ -39,6 +37,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor
import java.util.Locale
@ -69,12 +68,10 @@ class VisibleMessageContentView : ConstraintLayout {
onAttachmentNeedsDownload: (Long, Long) -> Unit
) {
// Background
val background = getBackground(message.isOutgoing)
val color = if (message.isOutgoing) context.getAccentColor()
else context.getColorFromAttr(R.attr.message_received_background_color)
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
background.colorFilter = filter
binding.contentParent.background = background
binding.contentParent.mainColor = color
binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius)
val onlyBodyMessage = message is SmsMessageRecord
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
@ -243,11 +240,6 @@ class VisibleMessageContentView : ConstraintLayout {
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
private fun getBackground(isOutgoing: Boolean): Drawable {
val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
}
fun recycle() {
arrayOf(
binding.deletedMessageView.root,
@ -265,6 +257,15 @@ class VisibleMessageContentView : ConstraintLayout {
fun playVoiceMessage() {
binding.voiceMessageView.root.togglePlayback()
}
fun playHighlight() {
// Show the highlight colour immediately then slowly fade out
val targetColor = if (ThemeUtil.isDarkTheme(context)) context.getAccentColor() else resources.getColor(R.color.black, context.theme)
val clearTargetColor = ColorUtils.setAlphaComponent(targetColor, 0)
binding.contentParent.numShadowRenders = if (ThemeUtil.isDarkTheme(context)) 3 else 1
binding.contentParent.sessionShadowColor = targetColor
GlowViewUtilities.animateShadowColorChange(binding.contentParent, targetColor, clearTargetColor, 1600)
}
// endregion
// region Convenience

View File

@ -111,6 +111,8 @@ class VisibleMessageView : LinearLayout {
private fun initialize() {
isHapticFeedbackEnabled = true
setWillNotDraw(false)
binding.root.disableClipping()
binding.mainContainer.disableClipping()
binding.messageInnerContainer.disableClipping()
binding.messageContentView.root.disableClipping()
}
@ -411,6 +413,10 @@ class VisibleMessageView : LinearLayout {
binding.profilePictureView.root.recycle()
binding.messageContentView.root.recycle()
}
fun playHighlight() {
binding.messageContentView.root.playHighlight()
}
// endregion
// region Interaction

View File

@ -7,6 +7,7 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.annotation.ColorInt
@ -55,16 +56,21 @@ object GlowViewUtilities {
animation.start()
}
fun animateShadowColorChange(view: GlowView, @ColorInt startColor: Int, @ColorInt endColor: Int) {
fun animateShadowColorChange(
view: GlowView,
@ColorInt startColor: Int,
@ColorInt endColor: Int,
duration: Long = 250
) {
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
animation.duration = 250
animation.duration = duration
animation.interpolator = AccelerateDecelerateInterpolator()
animation.addUpdateListener { animator ->
val color = animator.animatedValue as Int
view.sessionShadowColor = color
}
animation.start()
}
}
class PNModeView : LinearLayout, GlowView {
@ -223,3 +229,59 @@ class InputBarButtonImageViewContainer : RelativeLayout, GlowView {
}
// endregion
}
class MessageBubbleView : androidx.constraintlayout.widget.ConstraintLayout, GlowView {
@ColorInt override var mainColor: Int = 0
set(newValue) { field = newValue; paint.color = newValue }
@ColorInt override var sessionShadowColor: Int = 0
set(newValue) {
field = newValue
shadowPaint.setShadowLayer(toPx(10, resources).toFloat(), 0.0f, 0.0f, newValue)
if (numShadowRenders == 0) {
numShadowRenders = 1
}
invalidate()
}
var cornerRadius: Float = 0f
var numShadowRenders: Int = 0
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.FILL
result.isAntiAlias = true
result
}
private val shadowPaint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.FILL
result.isAntiAlias = true
result
}
// region Lifecycle
constructor(context: Context) : super(context) { }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { }
init {
setWillNotDraw(false)
}
// endregion
// region Updating
override fun onDraw(c: Canvas) {
val w = width.toFloat()
val h = height.toFloat()
(0 until numShadowRenders).forEach {
c.drawRoundRect(0f, 0f, w, h, cornerRadius, cornerRadius, shadowPaint)
}
c.drawRoundRect(0f, 0f, w, h, cornerRadius, cornerRadius, paint)
super.onDraw(c)
}
// endregion
}

View File

@ -7,7 +7,7 @@
android:id="@+id/mainContainerConstraint"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
<org.thoughtcrime.securesms.util.MessageBubbleView
android:id="@+id/contentParent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -111,7 +111,7 @@
android:id="@+id/bodyTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</org.thoughtcrime.securesms.util.MessageBubbleView>
<include layout="@layout/album_thumbnail_view"
android:visibility="gone"