543 lines
24 KiB
Kotlin
543 lines
24 KiB
Kotlin
package org.thoughtcrime.securesms.conversation.v2.messages
|
|
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.graphics.Canvas
|
|
import android.graphics.Rect
|
|
import android.graphics.drawable.ColorDrawable
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.util.AttributeSet
|
|
import android.view.Gravity
|
|
import android.view.HapticFeedbackConstants
|
|
import android.view.MotionEvent
|
|
import android.view.View
|
|
import android.widget.FrameLayout
|
|
import android.widget.LinearLayout
|
|
import androidx.annotation.ColorInt
|
|
import androidx.annotation.DrawableRes
|
|
import androidx.annotation.StringRes
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.constraintlayout.widget.ConstraintLayout
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.os.bundleOf
|
|
import androidx.core.view.isInvisible
|
|
import androidx.core.view.isVisible
|
|
import androidx.core.view.marginBottom
|
|
import dagger.hilt.android.AndroidEntryPoint
|
|
import network.loki.messenger.R
|
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
|
import org.session.libsession.messaging.contacts.Contact
|
|
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
|
import org.session.libsession.snode.SnodeAPI
|
|
import org.session.libsession.utilities.Address
|
|
import org.session.libsession.utilities.ViewUtil
|
|
import org.session.libsession.utilities.getColorFromAttr
|
|
import org.session.libsignal.utilities.IdPrefix
|
|
import org.session.libsignal.utilities.ThreadUtils
|
|
import org.thoughtcrime.securesms.ApplicationContext
|
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
|
import org.thoughtcrime.securesms.database.SmsDatabase
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
|
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
|
import org.thoughtcrime.securesms.mms.GlideApp
|
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
import org.thoughtcrime.securesms.util.DateUtils
|
|
import org.thoughtcrime.securesms.util.disableClipping
|
|
import org.thoughtcrime.securesms.util.toDp
|
|
import org.thoughtcrime.securesms.util.toPx
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
import javax.inject.Inject
|
|
import kotlin.math.abs
|
|
import kotlin.math.min
|
|
import kotlin.math.roundToInt
|
|
import kotlin.math.sqrt
|
|
|
|
@AndroidEntryPoint
|
|
class VisibleMessageView : LinearLayout {
|
|
|
|
@Inject lateinit var threadDb: ThreadDatabase
|
|
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
|
@Inject lateinit var lokiApiDb: LokiAPIDatabase
|
|
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
|
|
@Inject lateinit var smsDb: SmsDatabase
|
|
@Inject lateinit var mmsDb: MmsDatabase
|
|
|
|
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
|
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
|
private val swipeToReplyIconRect = Rect()
|
|
private var dx = 0.0f
|
|
private var previousTranslationX = 0.0f
|
|
private val gestureHandler = Handler(Looper.getMainLooper())
|
|
private var pressCallback: Runnable? = null
|
|
private var longPressCallback: Runnable? = null
|
|
private var onDownTimestamp = 0L
|
|
private var onDoubleTap: (() -> Unit)? = null
|
|
var indexInAdapter: Int = -1
|
|
var snIsSelected = false
|
|
set(value) {
|
|
field = value
|
|
handleIsSelectedChanged()
|
|
}
|
|
var onPress: ((event: MotionEvent) -> Unit)? = null
|
|
var onSwipeToReply: (() -> Unit)? = null
|
|
var onLongPress: (() -> Unit)? = null
|
|
val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView.root }
|
|
|
|
companion object {
|
|
const val swipeToReplyThreshold = 64.0f // dp
|
|
const val longPressMovementThreshold = 10.0f // dp
|
|
const val longPressDurationThreshold = 250L // ms
|
|
const val maxDoubleTapInterval = 200L
|
|
}
|
|
|
|
// 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)
|
|
|
|
override fun onFinishInflate() {
|
|
super.onFinishInflate()
|
|
initialize()
|
|
}
|
|
|
|
private fun initialize() {
|
|
isHapticFeedbackEnabled = true
|
|
setWillNotDraw(false)
|
|
binding.root.disableClipping()
|
|
binding.mainContainer.disableClipping()
|
|
binding.messageInnerContainer.disableClipping()
|
|
binding.messageInnerLayout.disableClipping()
|
|
binding.messageContentView.root.disableClipping()
|
|
}
|
|
// endregion
|
|
|
|
// region Updating
|
|
fun bind(
|
|
message: MessageRecord,
|
|
previous: MessageRecord? = null,
|
|
next: MessageRecord? = null,
|
|
glide: GlideRequests = GlideApp.with(this),
|
|
searchQuery: String? = null,
|
|
contact: Contact? = null,
|
|
senderSessionID: String,
|
|
lastSeen: Long,
|
|
delegate: VisibleMessageViewDelegate? = null,
|
|
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
|
) {
|
|
val threadID = message.threadId
|
|
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
|
val isGroupThread = thread.isGroupRecipient
|
|
val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread)
|
|
val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread)
|
|
// Show profile picture and sender name if this is a group thread AND
|
|
// the message is incoming
|
|
binding.moderatorIconImageView.isVisible = false
|
|
binding.profilePictureView.visibility = when {
|
|
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
|
|
thread.isGroupRecipient -> View.INVISIBLE
|
|
else -> View.GONE
|
|
}
|
|
|
|
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
|
|
else ViewUtil.dpToPx(context,2)
|
|
|
|
if (binding.profilePictureView.visibility == View.GONE) {
|
|
val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams
|
|
expirationParams.bottomMargin = bottomMargin
|
|
binding.messageInnerContainer.layoutParams = expirationParams
|
|
} else {
|
|
val avatarLayoutParams = binding.profilePictureView.layoutParams as MarginLayoutParams
|
|
avatarLayoutParams.bottomMargin = bottomMargin
|
|
binding.profilePictureView.layoutParams = avatarLayoutParams
|
|
}
|
|
|
|
if (isGroupThread && !message.isOutgoing) {
|
|
if (isEndOfMessageCluster) {
|
|
binding.profilePictureView.publicKey = senderSessionID
|
|
binding.profilePictureView.update(message.individualRecipient)
|
|
binding.profilePictureView.setOnClickListener {
|
|
if (thread.isOpenGroupRecipient) {
|
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
|
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
|
// TODO: support v2 soon
|
|
val intent = Intent(context, ConversationActivityV2::class.java)
|
|
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
|
|
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
|
|
context.startActivity(intent)
|
|
}
|
|
} else {
|
|
maybeShowUserDetails(senderSessionID, threadID)
|
|
}
|
|
}
|
|
if (thread.isOpenGroupRecipient) {
|
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
|
var standardPublicKey = ""
|
|
var blindedPublicKey: String? = null
|
|
if (IdPrefix.fromValue(senderSessionID)?.isBlinded() == true) {
|
|
blindedPublicKey = senderSessionID
|
|
} else {
|
|
standardPublicKey = senderSessionID
|
|
}
|
|
val isModerator = OpenGroupManager.isUserModerator(context, openGroup.groupId, standardPublicKey, blindedPublicKey)
|
|
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
|
|
}
|
|
}
|
|
}
|
|
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
|
|
val contactContext =
|
|
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
|
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
|
|
// Unread marker
|
|
binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
|
// Date break
|
|
val showDateBreak = isStartOfMessageCluster || snIsSelected
|
|
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
|
binding.dateBreakTextView.isVisible = showDateBreak
|
|
// Message status indicator
|
|
if (message.isOutgoing) {
|
|
val (iconID, iconColor, textId, contentDescription) = getMessageStatusImage(message)
|
|
if (textId != null) {
|
|
binding.messageStatusTextView.setText(textId)
|
|
|
|
if (iconColor != null) {
|
|
binding.messageStatusTextView.setTextColor(iconColor)
|
|
}
|
|
}
|
|
if (iconID != null) {
|
|
val drawable = ContextCompat.getDrawable(context, iconID)?.mutate()
|
|
if (iconColor != null) {
|
|
drawable?.setTint(iconColor)
|
|
}
|
|
binding.messageStatusImageView.setImageDrawable(drawable)
|
|
}
|
|
binding.messageStatusImageView.contentDescription = contentDescription
|
|
|
|
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
|
binding.messageStatusTextView.isVisible = (
|
|
textId != null && (
|
|
!message.isSent ||
|
|
message.id == lastMessageID
|
|
)
|
|
)
|
|
binding.messageStatusImageView.isVisible = (
|
|
iconID != null && (
|
|
!message.isSent ||
|
|
message.id == lastMessageID
|
|
)
|
|
)
|
|
} else {
|
|
binding.messageStatusTextView.isVisible = false
|
|
binding.messageStatusImageView.isVisible = false
|
|
}
|
|
// Expiration timer
|
|
updateExpirationTimer(message)
|
|
// Emoji Reactions
|
|
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
|
|
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
|
binding.emojiReactionsView.root.layoutParams = emojiLayoutParams
|
|
|
|
if (message.reactions.isNotEmpty()) {
|
|
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
|
|
if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) {
|
|
binding.emojiReactionsView.root.setReactions(message.id, message.reactions, message.isOutgoing, delegate)
|
|
binding.emojiReactionsView.root.isVisible = true
|
|
} else {
|
|
binding.emojiReactionsView.root.isVisible = false
|
|
}
|
|
}
|
|
else {
|
|
binding.emojiReactionsView.root.isVisible = false
|
|
}
|
|
|
|
// Populate content view
|
|
binding.messageContentView.root.indexInAdapter = indexInAdapter
|
|
binding.messageContentView.root.bind(
|
|
message,
|
|
isStartOfMessageCluster,
|
|
isEndOfMessageCluster,
|
|
glide,
|
|
thread,
|
|
searchQuery,
|
|
onAttachmentNeedsDownload
|
|
)
|
|
binding.messageContentView.root.delegate = delegate
|
|
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
|
|
}
|
|
|
|
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
|
|
return if (isGroupThread) {
|
|
previous == null || previous.isUpdate || !DateUtils.isSameHour(current.timestamp, previous.timestamp)
|
|
|| current.recipient.address != previous.recipient.address
|
|
} else {
|
|
previous == null || previous.isUpdate || !DateUtils.isSameHour(current.timestamp, previous.timestamp)
|
|
|| current.isOutgoing != previous.isOutgoing
|
|
}
|
|
}
|
|
|
|
private fun isEndOfMessageCluster(current: MessageRecord, next: MessageRecord?, isGroupThread: Boolean): Boolean {
|
|
return if (isGroupThread) {
|
|
next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp)
|
|
|| current.recipient.address != next.recipient.address
|
|
} else {
|
|
next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp)
|
|
|| current.isOutgoing != next.isOutgoing
|
|
}
|
|
}
|
|
|
|
data class MessageStatusInfo(@DrawableRes val iconId: Int?,
|
|
@ColorInt val iconTint: Int?,
|
|
@StringRes val messageText: Int?,
|
|
val contentDescription: String?)
|
|
|
|
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
|
|
message.isFailed ->
|
|
MessageStatusInfo(
|
|
R.drawable.ic_delivery_status_failed,
|
|
resources.getColor(R.color.destructive, context.theme),
|
|
R.string.delivery_status_failed,
|
|
null
|
|
)
|
|
message.isSyncFailed ->
|
|
MessageStatusInfo(
|
|
R.drawable.ic_delivery_status_failed,
|
|
context.getColor(R.color.accent_orange),
|
|
R.string.delivery_status_sync_failed,
|
|
null
|
|
)
|
|
message.isPending ->
|
|
MessageStatusInfo(
|
|
R.drawable.ic_delivery_status_sending,
|
|
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending,
|
|
context.getString(R.string.AccessibilityId_message_sent_status_pending)
|
|
)
|
|
message.isResyncing ->
|
|
MessageStatusInfo(
|
|
R.drawable.ic_delivery_status_sending,
|
|
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing,
|
|
context.getString(R.string.AccessibilityId_message_sent_status_syncing)
|
|
)
|
|
message.isRead ->
|
|
MessageStatusInfo(
|
|
R.drawable.ic_delivery_status_read,
|
|
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read,
|
|
null
|
|
)
|
|
else ->
|
|
MessageStatusInfo(
|
|
R.drawable.ic_delivery_status_sent,
|
|
context.getColorFromAttr(R.attr.message_status_color),
|
|
R.string.delivery_status_sent,
|
|
context.getString(R.string.AccessibilityId_message_sent_status_tick)
|
|
)
|
|
}
|
|
|
|
private fun updateExpirationTimer(message: MessageRecord) {
|
|
val container = binding.messageInnerContainer
|
|
val layout = binding.messageInnerLayout
|
|
|
|
if (message.isOutgoing) binding.messageContentView.root.bringToFront()
|
|
else binding.expirationTimerView.bringToFront()
|
|
|
|
layout.layoutParams = layout.layoutParams.let { it as FrameLayout.LayoutParams }
|
|
.apply { gravity = if (message.isOutgoing) Gravity.END else Gravity.START }
|
|
|
|
val containerParams = container.layoutParams as ConstraintLayout.LayoutParams
|
|
containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
|
container.layoutParams = containerParams
|
|
if (message.expiresIn > 0 && !message.isPending) {
|
|
binding.expirationTimerView.setColorFilter(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
|
binding.expirationTimerView.isInvisible = false
|
|
binding.expirationTimerView.setPercentComplete(0.0f)
|
|
if (message.expireStarted > 0) {
|
|
binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
|
binding.expirationTimerView.startAnimation()
|
|
if (message.expireStarted + message.expiresIn <= SnodeAPI.nowWithOffset) {
|
|
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
|
|
}
|
|
} else if (!message.isMediaPending) {
|
|
binding.expirationTimerView.setPercentComplete(0.0f)
|
|
binding.expirationTimerView.stopAnimation()
|
|
ThreadUtils.queue {
|
|
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
|
|
val id = message.getId()
|
|
val mms = message.isMms
|
|
if (mms) mmsDb.markExpireStarted(id) else smsDb.markExpireStarted(id)
|
|
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
|
|
}
|
|
} else {
|
|
binding.expirationTimerView.stopAnimation()
|
|
binding.expirationTimerView.setPercentComplete(0.0f)
|
|
}
|
|
} else {
|
|
binding.expirationTimerView.isInvisible = true
|
|
}
|
|
container.requestLayout()
|
|
}
|
|
|
|
private fun handleIsSelectedChanged() {
|
|
background = if (snIsSelected) {
|
|
ColorDrawable(context.getColorFromAttr(R.attr.message_selected))
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
override fun onDraw(canvas: Canvas) {
|
|
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
|
val iconSize = toPx(24, context.resources)
|
|
val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
|
|
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.marginBottom - (iconSize / 2)
|
|
val right = left + iconSize
|
|
val bottom = top + iconSize
|
|
swipeToReplyIconRect.left = left
|
|
swipeToReplyIconRect.top = top
|
|
swipeToReplyIconRect.right = right
|
|
swipeToReplyIconRect.bottom = bottom
|
|
|
|
if (translationX < 0 && !binding.expirationTimerView.isVisible) {
|
|
val threshold = swipeToReplyThreshold
|
|
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
|
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
|
} else {
|
|
swipeToReplyIcon.alpha = 0
|
|
}
|
|
swipeToReplyIcon.draw(canvas)
|
|
super.onDraw(canvas)
|
|
}
|
|
|
|
fun recycle() {
|
|
binding.profilePictureView.recycle()
|
|
binding.messageContentView.root.recycle()
|
|
}
|
|
|
|
fun playHighlight() {
|
|
binding.messageContentView.root.playHighlight()
|
|
}
|
|
// endregion
|
|
|
|
// region Interaction
|
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
if (onPress == null || onSwipeToReply == null || onLongPress == null) { return false }
|
|
when (event.action) {
|
|
MotionEvent.ACTION_DOWN -> onDown(event)
|
|
MotionEvent.ACTION_MOVE -> onMove(event)
|
|
MotionEvent.ACTION_CANCEL -> onCancel(event)
|
|
MotionEvent.ACTION_UP -> onUp(event)
|
|
}
|
|
return true
|
|
}
|
|
|
|
private fun onDown(event: MotionEvent) {
|
|
dx = x - event.rawX
|
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
|
val newLongPressCallback = Runnable { onLongPress() }
|
|
this.longPressCallback = newLongPressCallback
|
|
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
|
|
onDownTimestamp = Date().time
|
|
}
|
|
|
|
private fun onMove(event: MotionEvent) {
|
|
val translationX = toDp(event.rawX + dx, context.resources)
|
|
if (abs(translationX) < longPressMovementThreshold || snIsSelected) {
|
|
return
|
|
} else {
|
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
|
}
|
|
if (translationX > 0) { return } // Only allow swipes to the left
|
|
// The idea here is to asymptotically approach a maximum drag distance
|
|
val damping = 50.0f
|
|
val sign = -1.0f
|
|
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
|
|
this.translationX = x
|
|
binding.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving
|
|
postInvalidate() // Ensure onDraw(canvas:) is called
|
|
if (abs(x) > swipeToReplyThreshold && abs(previousTranslationX) < swipeToReplyThreshold) {
|
|
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
|
}
|
|
previousTranslationX = x
|
|
}
|
|
|
|
private fun onCancel(event: MotionEvent) {
|
|
if (abs(translationX) > swipeToReplyThreshold) {
|
|
onSwipeToReply?.invoke()
|
|
}
|
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
|
resetPosition()
|
|
}
|
|
|
|
private fun onUp(event: MotionEvent) {
|
|
if (abs(translationX) > swipeToReplyThreshold) {
|
|
onSwipeToReply?.invoke()
|
|
} else if ((Date().time - onDownTimestamp) < longPressDurationThreshold) {
|
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
|
val pressCallback = this.pressCallback
|
|
if (pressCallback != null) {
|
|
// If we're here and pressCallback isn't null, it means that we tapped again within
|
|
// maxDoubleTapInterval ms and we should count this as a double tap
|
|
gestureHandler.removeCallbacks(pressCallback)
|
|
this.pressCallback = null
|
|
onDoubleTap?.invoke()
|
|
} else {
|
|
val newPressCallback = Runnable { onPress(event) }
|
|
this.pressCallback = newPressCallback
|
|
gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval)
|
|
}
|
|
}
|
|
resetPosition()
|
|
}
|
|
|
|
private fun resetPosition() {
|
|
animate()
|
|
.translationX(0.0f)
|
|
.setDuration(150)
|
|
.setUpdateListener {
|
|
postInvalidate() // Ensure onDraw(canvas:) is called
|
|
}
|
|
.start()
|
|
// Bit of a hack to keep the date break text view from moving
|
|
binding.dateBreakTextView.animate()
|
|
.translationX(0.0f)
|
|
.setDuration(150)
|
|
.start()
|
|
}
|
|
|
|
private fun onLongPress() {
|
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
onLongPress?.invoke()
|
|
}
|
|
|
|
fun onContentClick(event: MotionEvent) {
|
|
binding.messageContentView.root.onContentClick(event)
|
|
}
|
|
|
|
private fun onPress(event: MotionEvent) {
|
|
onPress?.invoke(event)
|
|
pressCallback = null
|
|
}
|
|
|
|
private fun maybeShowUserDetails(publicKey: String, threadID: Long) {
|
|
val userDetailsBottomSheet = UserDetailsBottomSheet()
|
|
val bundle = bundleOf(
|
|
UserDetailsBottomSheet.ARGUMENT_PUBLIC_KEY to publicKey,
|
|
UserDetailsBottomSheet.ARGUMENT_THREAD_ID to threadID
|
|
)
|
|
userDetailsBottomSheet.arguments = bundle
|
|
val activity = context as AppCompatActivity
|
|
userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag)
|
|
}
|
|
|
|
fun playVoiceMessage() {
|
|
binding.messageContentView.root.playVoiceMessage()
|
|
}
|
|
// endregion
|
|
}
|