From 635cee15852d2c5746e4729ba8794cbbf7c2bbfb Mon Sep 17 00:00:00 2001 From: AL-Session <160798022+AL-Session@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:59:43 +1100 Subject: [PATCH] fix/prevent_button_spam_on_scroll_to_replied_message - and VisibleMessageViews in general (#983) * WIP * Minor tidyup * Removed some blank lines * Fix typo * Tweaks --------- Co-authored-by: alansley Co-authored-by: ThomasSession --- .../conversation/v2/messages/QuoteView.kt | 1 - .../v2/messages/VisibleMessageView.kt | 18 +++++++++++++--- .../securesms/mediasend/MediaSendActivity.kt | 9 ++++++-- .../securesms/util/SafeClickListener.kt | 21 +++++++++++++++++++ .../securesms/util/ViewUtilities.kt | 10 +++++++++ .../libsession/messaging/messages/Message.kt | 1 + 6 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/SafeClickListener.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index 35383302fd..4b572a1a3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -155,6 +155,5 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } interface QuoteViewDelegate { - fun cancelQuoteDraft() } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 5c8810557c..8ab0baa3d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -8,6 +8,7 @@ import android.graphics.Rect import android.graphics.drawable.ColorDrawable import android.os.Handler import android.os.Looper +import android.os.SystemClock import android.util.AttributeSet import android.view.Gravity import android.view.HapticFeedbackConstants @@ -123,6 +124,10 @@ class VisibleMessageView : FrameLayout { var onLongPress: (() -> Unit)? = null val messageContentView: VisibleMessageContentView get() = binding.messageContentView.root + // Prevent button spam + val MINIMUM_DURATION_BETWEEN_CLICKS_ON_SAME_VIEW_MS = 500L + var lastClickTimestampMS = 0L + companion object { const val swipeToReplyThreshold = 64.0f // dp const val longPressMovementThreshold = 10.0f // dp @@ -613,15 +618,22 @@ class VisibleMessageView : FrameLayout { onLongPress?.invoke() } - fun onContentClick(event: MotionEvent) { - binding.messageContentView.root.onContentClick(event) - } + private fun clickedTooFast() = (SystemClock.elapsedRealtime() - lastClickTimestampMS < MINIMUM_DURATION_BETWEEN_CLICKS_ON_SAME_VIEW_MS) + // Note: `onPress` is called BEFORE `onContentClick` is called, so we only filter here rather than + // in both places otherwise `onContentClick` will instantly fail the button spam test. private fun onPress(event: MotionEvent) { + // Don't process the press if it's too soon after the last one.. + if (clickedTooFast()) return + + // ..otherwise take note of the time and process the event. + lastClickTimestampMS = SystemClock.elapsedRealtime() onPress?.invoke(event) pressCallback = null } + fun onContentClick(event: MotionEvent) = binding.messageContentView.root.onContentClick(event) + private fun maybeShowUserDetails(publicKey: String, threadID: Long) { UserDetailsBottomSheet().apply { arguments = bundleOf( diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt index 2569b4f110..9dbceca531 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt @@ -160,8 +160,13 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme } override fun onMediaSelected(media: Media) { - viewModel.onSingleMediaSelected(this, media) - navigateToMediaSend(recipient!!) + try { + viewModel.onSingleMediaSelected(this, media) + navigateToMediaSend(recipient!!) + } catch (e: Exception){ + Log.e(TAG, "Error selecting media", e) + Toast.makeText(this, R.string.errorUnknown, Toast.LENGTH_LONG).show() + } } override fun onAddMediaClicked(bucketId: String) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SafeClickListener.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SafeClickListener.kt new file mode 100644 index 0000000000..a53cc35625 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SafeClickListener.kt @@ -0,0 +1,21 @@ +package org.thoughtcrime.securesms.util + +import android.os.SystemClock +import android.view.View + +// Listener class that only accepts clicks at a given interval to prevent button spam. +// Note: While this cannot be used on conversation views without interfering with motion events it may still be useful. +class SafeClickListener( + private var minimumClickIntervalMS: Long = 500L, + private val onSafeClick: (View) -> Unit +) : View.OnClickListener { + private var lastClickTimestampMS: Long = 0L + + override fun onClick(v: View) { + // Ignore any follow-up clicks if the minimum interval has not passed + if (SystemClock.elapsedRealtime() - lastClickTimestampMS < minimumClickIntervalMS) return + + lastClickTimestampMS = SystemClock.elapsedRealtime() + onSafeClick(v) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt index 4aed6e7dac..e1ff58066e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt @@ -121,3 +121,13 @@ fun EditText.addTextChangedListener(listener: (String) -> Unit) { } }) } + +// Listener class that only accepts clicks at given interval to prevent button spam - can be used instead +// of a standard `onClickListener` in many places. A separate mechanism exists for VisibleMessageViews to +// prevent interfering with gestures. +fun View.setSafeOnClickListener(clickIntervalMS: Long = 1000L, onSafeClick: (View) -> Unit) { + val safeClickListener = SafeClickListener(minimumClickIntervalMS = clickIntervalMS) { + onSafeClick(it) + } + setOnClickListener(safeClickListener) +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index de05b78585..f80b26e718 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -17,6 +17,7 @@ abstract class Message { var recipient: String? = null var sender: String? = null var isSenderSelf: Boolean = false + var groupPublicKey: String? = null var openGroupServerMessageID: Long? = null var serverHash: String? = null