mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 11:05:25 +00:00
Updated the conversation to highlight the first unread message on open
This commit is contained in:
parent
3bd2883707
commit
da02d385d1
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user