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.Locale
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -341,6 +342,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private val messageToScrollTimestamp = AtomicLong(-1)
|
private val messageToScrollTimestamp = AtomicLong(-1)
|
||||||
private val messageToScrollAuthor = AtomicReference<Address?>(null)
|
private val messageToScrollAuthor = AtomicReference<Address?>(null)
|
||||||
private val firstLoad = AtomicBoolean(true)
|
private val firstLoad = AtomicBoolean(true)
|
||||||
|
private val forceHighlightNextLoad = AtomicInteger(-1)
|
||||||
|
|
||||||
private lateinit var reactionDelegate: ConversationReactionDelegate
|
private lateinit var reactionDelegate: ConversationReactionDelegate
|
||||||
private val reactWithAnyEmojiStartPage = -1
|
private val reactWithAnyEmojiStartPage = -1
|
||||||
@ -419,15 +421,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val weakActivity = WeakReference(this)
|
val weakActivity = WeakReference(this)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
|
|
||||||
|
|
||||||
// Note: We are accessing the `adapter` property because we want it to be loaded on
|
// 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
|
// the background thread to avoid blocking the UI thread and potentially hanging when
|
||||||
// transitioning to the activity
|
// transitioning to the activity
|
||||||
weakActivity.get()?.adapter ?: return@launch
|
weakActivity.get()?.adapter ?: return@launch
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateUnreadCountIndicator()
|
|
||||||
setUpRecyclerView()
|
setUpRecyclerView()
|
||||||
setUpTypingObserver()
|
setUpTypingObserver()
|
||||||
setUpRecipientObserver()
|
setUpRecipientObserver()
|
||||||
@ -503,19 +502,41 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val messageTimestamp = messageToScrollTimestamp.getAndSet(-1)
|
val messageTimestamp = messageToScrollTimestamp.getAndSet(-1)
|
||||||
val author = messageToScrollAuthor.getAndSet(null)
|
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) {
|
if (author != null && messageTimestamp >= 0) {
|
||||||
jumpToMessage(author, messageTimestamp, null)
|
jumpToMessage(author, messageTimestamp, null)
|
||||||
}
|
}
|
||||||
else if (firstLoad.getAndSet(false)) {
|
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()
|
handleRecyclerViewScrolled()
|
||||||
|
|
||||||
|
if (lastSeenItemPosition != null) {
|
||||||
|
forceHighlightNextLoad.set(lastSeenItemPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (oldCount != newCount) {
|
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()
|
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()
|
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 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
|
// 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
|
// of the first unread message in the middle of the screen
|
||||||
if (isFirstLoad && !reverseMessageList) {
|
if (isFirstLoad && !reverseMessageList) {
|
||||||
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
|
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)
|
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 {
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.style.BackgroundColorSpan
|
import android.text.style.BackgroundColorSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
@ -15,9 +14,7 @@ import android.view.View
|
|||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
|
||||||
import androidx.core.graphics.BlendModeCompat
|
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
@ -28,6 +25,7 @@ import okhttp3.HttpUrl
|
|||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
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.getColorFromAttr
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
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.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -69,12 +68,10 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||||
) {
|
) {
|
||||||
// Background
|
// Background
|
||||||
val background = getBackground(message.isOutgoing)
|
|
||||||
val color = if (message.isOutgoing) context.getAccentColor()
|
val color = if (message.isOutgoing) context.getAccentColor()
|
||||||
else context.getColorFromAttr(R.attr.message_received_background_color)
|
else context.getColorFromAttr(R.attr.message_received_background_color)
|
||||||
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
|
binding.contentParent.mainColor = color
|
||||||
background.colorFilter = filter
|
binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius)
|
||||||
binding.contentParent.background = background
|
|
||||||
|
|
||||||
val onlyBodyMessage = message is SmsMessageRecord
|
val onlyBodyMessage = message is SmsMessageRecord
|
||||||
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
|
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
|
||||||
@ -243,11 +240,6 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
||||||
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
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() {
|
fun recycle() {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
binding.deletedMessageView.root,
|
binding.deletedMessageView.root,
|
||||||
@ -265,6 +257,15 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
fun playVoiceMessage() {
|
fun playVoiceMessage() {
|
||||||
binding.voiceMessageView.root.togglePlayback()
|
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
|
// endregion
|
||||||
|
|
||||||
// region Convenience
|
// region Convenience
|
||||||
|
@ -111,6 +111,8 @@ class VisibleMessageView : LinearLayout {
|
|||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
isHapticFeedbackEnabled = true
|
isHapticFeedbackEnabled = true
|
||||||
setWillNotDraw(false)
|
setWillNotDraw(false)
|
||||||
|
binding.root.disableClipping()
|
||||||
|
binding.mainContainer.disableClipping()
|
||||||
binding.messageInnerContainer.disableClipping()
|
binding.messageInnerContainer.disableClipping()
|
||||||
binding.messageContentView.root.disableClipping()
|
binding.messageContentView.root.disableClipping()
|
||||||
}
|
}
|
||||||
@ -411,6 +413,10 @@ class VisibleMessageView : LinearLayout {
|
|||||||
binding.profilePictureView.root.recycle()
|
binding.profilePictureView.root.recycle()
|
||||||
binding.messageContentView.root.recycle()
|
binding.messageContentView.root.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun playHighlight() {
|
||||||
|
binding.messageContentView.root.playHighlight()
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
|
@ -7,6 +7,7 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
@ -55,16 +56,21 @@ object GlowViewUtilities {
|
|||||||
animation.start()
|
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)
|
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
|
||||||
animation.duration = 250
|
animation.duration = duration
|
||||||
|
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||||
animation.addUpdateListener { animator ->
|
animation.addUpdateListener { animator ->
|
||||||
val color = animator.animatedValue as Int
|
val color = animator.animatedValue as Int
|
||||||
view.sessionShadowColor = color
|
view.sessionShadowColor = color
|
||||||
}
|
}
|
||||||
animation.start()
|
animation.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PNModeView : LinearLayout, GlowView {
|
class PNModeView : LinearLayout, GlowView {
|
||||||
@ -223,3 +229,59 @@ class InputBarButtonImageViewContainer : RelativeLayout, GlowView {
|
|||||||
}
|
}
|
||||||
// endregion
|
// 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"
|
android:id="@+id/mainContainerConstraint"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<org.thoughtcrime.securesms.util.MessageBubbleView
|
||||||
android:id="@+id/contentParent"
|
android:id="@+id/contentParent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -111,7 +111,7 @@
|
|||||||
android:id="@+id/bodyTextView"
|
android:id="@+id/bodyTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</org.thoughtcrime.securesms.util.MessageBubbleView>
|
||||||
|
|
||||||
<include layout="@layout/album_thumbnail_view"
|
<include layout="@layout/album_thumbnail_view"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
Loading…
Reference in New Issue
Block a user