diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt index f3d65179e3..f35b15210f 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt @@ -313,7 +313,7 @@ class V2ConversationItemShapeTest { override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit - override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit + override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt index 2996f0d0c7..273dd22dcb 100644 --- a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt @@ -280,7 +280,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() } - override fun onEditedIndicatorClicked(messageRecord: MessageRecord) { + override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) { Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 92f02e6336..2805aa41e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -125,7 +125,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord); void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord); void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args); - void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord); + void onEditedIndicatorClicked(@NonNull ConversationMessage conversationMessage); void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks); void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey); void onShowSafetyTips(boolean forGroup); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 408b3e711c..84120925a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -1730,7 +1730,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo if (MessageRecordUtil.isEditMessage(current)) { activeFooter.getDateView().setOnClickListener(v -> { if (eventListener != null) { - eventListener.onEditedIndicatorClicked(current); + eventListener.onEditedIndicatorClicked(conversationMessage); } }); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt index 4dbf8f5a17..fc5e3f3542 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt @@ -273,7 +273,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit override fun onActivatePaymentsClicked() = Unit override fun onSendPaymentClicked(recipientId: RecipientId) = Unit - override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit + override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit override fun onShowSafetyTips(forGroup: Boolean) = Unit override fun onReportSpamLearnMoreClicked() = Unit override fun onMessageRequestAcceptOptionsClicked() = Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt index 5407996a63..8a9daf8db3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode +import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.colors.Colorizer import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart @@ -253,9 +254,9 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() { getAdapterListener().onSendPaymentClicked(recipientId) } - override fun onEditedIndicatorClicked(messageRecord: MessageRecord) { + override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) { dismiss() - getAdapterListener().onEditedIndicatorClicked(messageRecord) + getAdapterListener().onEditedIndicatorClicked(conversationMessage) } override fun onShowSafetyTips(forGroup: Boolean) = Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt index f9b2f6f3d1..7d0475d531 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt @@ -164,7 +164,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() { override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit override fun onActivatePaymentsClicked() = Unit override fun onSendPaymentClicked(recipientId: RecipientId) = Unit - override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit + override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit override fun onShowSafetyTips(forGroup: Boolean) = Unit override fun onReportSpamLearnMoreClicked() = Unit override fun onMessageRequestAcceptOptionsClicked() = Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index d9fc55cfa6..ec3a10e636 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -294,6 +294,7 @@ import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.Debouncer import org.thoughtcrime.securesms.util.DeleteDialog import org.thoughtcrime.securesms.util.Dialogs +import org.thoughtcrime.securesms.util.DoubleClickDebouncer import org.thoughtcrime.securesms.util.DrawableUtil import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.FullscreenHelper @@ -469,6 +470,7 @@ class ConversationFragment : private val conversationTooltips = ConversationTooltips(this) private val colorizer = Colorizer() private val textDraftSaveDebouncer = Debouncer(500) + private val doubleTapToEditDebouncer = DoubleClickDebouncer(200) private val recentEmojis: RecentEmojiPageModel by lazy { RecentEmojiPageModel(AppDependencies.application, TextSecurePreferences.RECENT_STORAGE_KEY) } private lateinit var layoutManager: ConversationLayoutManager @@ -2783,16 +2785,20 @@ class ConversationFragment : override fun onItemDoubleClick(item: MultiselectPart) { Log.d(TAG, "onItemDoubleClick") - if (!isValidEditMessageSend(item.getMessageRecord(), System.currentTimeMillis())) { + onDoubleTapToEdit(item.conversationMessage) + } + + private fun onDoubleTapToEdit(conversationMessage: ConversationMessage) { + if (!isValidEditMessageSend(conversationMessage.getMessageRecord(), System.currentTimeMillis())) { return } if (SignalStore.uiHints().hasSeenDoubleTapEditEducationSheet) { - onDoubleTapEditEducationSheetNext(item.conversationMessage) + onDoubleTapEditEducationSheetNext(conversationMessage) return } - DoubleTapEditEducationSheet(item).show(childFragmentManager, DoubleTapEditEducationSheet.KEY) + DoubleTapEditEducationSheet(conversationMessage).show(childFragmentManager, DoubleTapEditEducationSheet.KEY) } override fun onMessageWithErrorClicked(messageRecord: MessageRecord) { @@ -3003,9 +3009,12 @@ class ConversationFragment : requireActivity().startActivity(MediaIntentFactory.create(requireActivity(), args), options.toBundle()) } - override fun onEditedIndicatorClicked(messageRecord: MessageRecord) { - if (messageRecord.isOutgoing) { - EditMessageHistoryDialog.show(childFragmentManager, messageRecord.toRecipient.id, messageRecord) + override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) { + val messageRecord = conversationMessage.messageRecord + if (conversationMessage.messageRecord.isOutgoing) { + if (!doubleTapToEditDebouncer.onClick { EditMessageHistoryDialog.show(childFragmentManager, messageRecord.toRecipient.id, messageRecord) }) { + onDoubleTapToEdit(conversationMessage) + } } else { EditMessageHistoryDialog.show(childFragmentManager, messageRecord.fromRecipient.id, messageRecord) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DoubleTapEditEducationSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DoubleTapEditEducationSheet.kt index 95a82a7b74..bae949bb52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DoubleTapEditEducationSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DoubleTapEditEducationSheet.kt @@ -9,14 +9,13 @@ import com.google.android.material.button.MaterialButton import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment import org.thoughtcrime.securesms.conversation.ConversationMessage -import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.fragments.requireListener /** * Shows an education sheet to users explaining how double tapping a sent message within 24hrs will allow them to edit it */ -class DoubleTapEditEducationSheet(private val item: MultiselectPart) : FixedRoundedCornerBottomSheetDialogFragment() { +class DoubleTapEditEducationSheet(private val conversationMessage: ConversationMessage) : FixedRoundedCornerBottomSheetDialogFragment() { companion object { const val KEY = "DOUBLE_TAP_EDIT_EDU" @@ -32,14 +31,14 @@ class DoubleTapEditEducationSheet(private val item: MultiselectPart) : FixedRoun SignalStore.uiHints().hasSeenDoubleTapEditEducationSheet = true view.findViewById(R.id.got_it).setOnClickListener { - requireListener().onDoubleTapEditEducationSheetNext(item.conversationMessage) + requireListener().onDoubleTapEditEducationSheetNext(conversationMessage) dismissAllowingStateLoss() } } override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) - requireListener().onDoubleTapEditEducationSheetNext(item.conversationMessage) + requireListener().onDoubleTapEditEducationSheetNext(conversationMessage) } interface Callback { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt index 5334e14c1c..29f7f5bb44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt @@ -677,7 +677,7 @@ open class V2ConversationItemTextOnlyViewHolder>( } binding.footerDate.setOnClickListener { - conversationContext.clickListener.onEditedIndicatorClicked(record) + conversationContext.clickListener.onEditedIndicatorClicked(conversationMessage) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DoubleClickDebouncer.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DoubleClickDebouncer.kt new file mode 100644 index 0000000000..db538603b7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DoubleClickDebouncer.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.util + +import android.os.Handler +import android.os.Looper +import java.util.concurrent.TimeUnit + +/** + * A class to throttle on click events. If multiple clicks happen in succession (within the threshold) + * We ignore both and let the caller know it was a double click. + */ +class DoubleClickDebouncer(private val threshold: Long) { + private val handler = Handler(Looper.getMainLooper()) + + constructor(threshold: Long, timeUnit: TimeUnit) : this(timeUnit.toMillis(threshold)) + + private var clickEnqueued = false + + /** + * Returns true if the click is enqueued, otherwise its a double click + */ + fun onClick(runnable: Runnable?): Boolean { + handler.removeCallbacksAndMessages(null) + if (!clickEnqueued) { + handler.postDelayed({ + runnable!!.run() + clickEnqueued = false + }, threshold) + clickEnqueued = true + } else { + clickEnqueued = false + } + return clickEnqueued + } + + fun clear() { + handler.removeCallbacksAndMessages(null) + } +}