From ffc1463cdab238dd15d727efee0f9c2968f49117 Mon Sep 17 00:00:00 2001 From: mtang-signal Date: Thu, 25 Apr 2024 17:24:21 -0400 Subject: [PATCH] Add double tap editing feature. --- .../v2/items/V2ConversationItemShapeTest.kt | 2 + .../test/InternalConversationTestFragment.kt | 4 + .../securesms/BindableConversationItem.java | 1 + .../conversation/ConversationItem.java | 21 ++++++ .../ScheduledMessagesBottomSheet.kt | 1 + .../quotes/MessageQuotesBottomSheet.kt | 1 + .../ui/edit/EditMessageHistoryDialog.kt | 1 + .../conversation/v2/ConversationFragment.kt | 21 +++++- .../v2/DoubleTapEditEducationSheet.kt | 48 ++++++++++++ .../V2ConversationItemTextOnlyViewHolder.kt | 16 ++++ .../securesms/keyvalue/UiHints.java | 37 ++++++---- ...n_item_double_tap_edit_education_sheet.xml | 74 +++++++++++++++++++ app/src/main/res/values/strings.xml | 8 ++ 13 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DoubleTapEditEducationSheet.kt create mode 100644 app/src/main/res/layout/conversation_item_double_tap_edit_education_sheet.xml 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 9a21acf4f9..f3d65179e3 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 @@ -328,5 +328,7 @@ class V2ConversationItemShapeTest { override fun onReportSpamLearnMoreClicked() = Unit override fun onMessageRequestAcceptOptionsClicked() = Unit + + override fun onItemDoubleClick(item: MultiselectPart) = 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 108c4e5a12..2996f0d0c7 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 @@ -300,6 +300,10 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() } + override fun onItemDoubleClick(item: MultiselectPart) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + override fun onShowSafetyTips(forGroup: Boolean) { 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 8b5ff1aaca..1a914b5fb3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -126,5 +126,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onShowSafetyTips(boolean forGroup); void onReportSpamLearnMoreClicked(); void onMessageRequestAcceptOptionsClicked(); + void onItemDoubleClick(MultiselectPart multiselectPart); } } 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 ce2221fc27..da423db299 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -40,6 +40,7 @@ import android.text.style.StyleSpan; import android.text.style.URLSpan; import android.util.AttributeSet; import android.util.TypedValue; +import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.TouchDelegate; @@ -256,6 +257,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private final UrlClickListener urlClickListener = new UrlClickListener(); private final Rect thumbnailMaskingRect = new Rect(); private final TouchDelegateChangedListener touchDelegateChangedListener = new TouchDelegateChangedListener(); + private final DoubleTapEditTouchListener doubleTapEditTouchListener = new DoubleTapEditTouchListener(); private final GiftMessageViewCallback giftMessageViewCallback = new GiftMessageViewCallback(); private final Context context; @@ -351,6 +353,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo setOnClickListener(new ClickListener(null)); + bodyText.setOnTouchListener(doubleTapEditTouchListener); bodyText.setOnLongClickListener(passthroughClickListener); bodyText.setOnClickListener(passthroughClickListener); footer.setOnTouchDelegateChangedListener(touchDelegateChangedListener); @@ -2438,6 +2441,24 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } } + private class DoubleTapEditTouchListener implements View.OnTouchListener { + private final GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDoubleTap(MotionEvent e) { + if (eventListener != null && batchSelected.isEmpty()) { + eventListener.onItemDoubleClick(getMultiselectPartForLatestTouch()); + return true; + } + return false; + } + }); + + @Override + public boolean onTouch(View v, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + } + private class AttachmentDownloadClickListener implements SlidesClickedListener { @Override public void onClick(View v, final List slides) { 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 82c7f9a6f8..4dbf8f5a17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt @@ -277,6 +277,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment override fun onShowSafetyTips(forGroup: Boolean) = Unit override fun onReportSpamLearnMoreClicked() = Unit override fun onMessageRequestAcceptOptionsClicked() = Unit + override fun onItemDoubleClick(item: MultiselectPart) = Unit } companion object { 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 169f2b37ea..0e050593c8 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 @@ -261,6 +261,7 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() { override fun onShowSafetyTips(forGroup: Boolean) = Unit override fun onReportSpamLearnMoreClicked() = Unit override fun onMessageRequestAcceptOptionsClicked() = Unit + override fun onItemDoubleClick(item: MultiselectPart) = Unit } companion object { 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 8369cc4e8d..f9b2f6f3d1 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 @@ -168,6 +168,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() { override fun onShowSafetyTips(forGroup: Boolean) = Unit override fun onReportSpamLearnMoreClicked() = Unit override fun onMessageRequestAcceptOptionsClicked() = Unit + override fun onItemDoubleClick(item: MultiselectPart) = Unit } companion object { 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 b7f55bcc5d..171dd4ad20 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 @@ -351,7 +351,8 @@ class ConversationFragment : ConversationBottomSheetCallback, SafetyNumberBottomSheet.Callbacks, EnableCallNotificationSettingsDialog.Callback, - MultiselectForwardBottomSheet.Callback { + MultiselectForwardBottomSheet.Callback, + DoubleTapEditEducationSheet.Callback { companion object { private val TAG = Log.tag(ConversationFragment::class.java) @@ -2755,6 +2756,20 @@ class ConversationFragment : RecipientBottomSheetDialogFragment.show(childFragmentManager, recipientId, groupId) } + override fun onItemDoubleClick(item: MultiselectPart) { + Log.d(TAG, "onItemDoubleClick") + if (!isValidEditMessageSend(item.getMessageRecord(), System.currentTimeMillis())) { + return + } + + if (SignalStore.uiHints().hasSeenDoubleTapEditEducationSheet) { + onDoubleTapEditEducationSheetNext(item.conversationMessage) + return + } + + DoubleTapEditEducationSheet(item).show(childFragmentManager, DoubleTapEditEducationSheet.KEY) + } + override fun onMessageWithErrorClicked(messageRecord: MessageRecord) { val recipientId = viewModel.recipientSnapshot?.id ?: return if (messageRecord.isIdentityMismatchFailure) { @@ -4307,4 +4322,8 @@ class ConversationFragment : } } } + + override fun onDoubleTapEditEducationSheetNext(conversationMessage: ConversationMessage) { + handleEditMessage(conversationMessage) + } } 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 new file mode 100644 index 0000000000..95a82a7b74 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DoubleTapEditEducationSheet.kt @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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() { + + companion object { + const val KEY = "DOUBLE_TAP_EDIT_EDU" + } + + override val peekHeightPercentage: Float = 1f + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.conversation_item_double_tap_edit_education_sheet, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + SignalStore.uiHints().hasSeenDoubleTapEditEducationSheet = true + + view.findViewById(R.id.got_it).setOnClickListener { + requireListener().onDoubleTapEditEducationSheetNext(item.conversationMessage) + dismissAllowingStateLoss() + } + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + requireListener().onDoubleTapEditEducationSheetNext(item.conversationMessage) + } + + interface Callback { + fun onDoubleTapEditEducationSheetNext(conversationMessage: ConversationMessage) + } +} 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 5ec98f4424..998ab89aa0 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 @@ -18,6 +18,8 @@ import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.URLSpan import android.util.TypedValue +import android.view.GestureDetector +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat @@ -110,6 +112,19 @@ open class V2ConversationItemTextOnlyViewHolder>( private val senderDrawable = ChatColorsDrawable(conversationContext::getChatColorsData) private val bodyBubbleLayoutTransition = BodyBubbleLayoutTransition() + private val gestureDetector = GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onDoubleTap(e: MotionEvent): Boolean { + if (conversationContext.selectedItems.isEmpty()) { + conversationContext.clickListener.onItemDoubleClick(getMultiselectPartForLatestTouch()) + return true + } + return false + } + } + ) + protected lateinit var shape: V2ConversationItemShape.MessageShape private val replyDelegate = object : V2ConversationItemLayout.OnMeasureListener { @@ -139,6 +154,7 @@ open class V2ConversationItemTextOnlyViewHolder>( ) } + binding.body.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } binding.root.setOnClickListener { onBubbleClicked() } binding.root.setOnLongClickListener { conversationContext.clickListener.onItemLongClick(binding.root, getMultiselectPartForLatestTouch()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java index 86b1b9d0f3..de3acba570 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java @@ -9,20 +9,21 @@ public class UiHints extends SignalStoreValues { private static final int NEVER_DISPLAY_PULL_TO_FILTER_TIP_THRESHOLD = 3; - private static final String HAS_SEEN_GROUP_SETTINGS_MENU_TOAST = "uihints.has_seen_group_settings_menu_toast"; - private static final String HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE = "uihints.has_confirmed_delete_for_everyone_once"; - private static final String HAS_SET_OR_SKIPPED_USERNAME_CREATION = "uihints.has_set_or_skipped_username_creation"; - private static final String NEVER_DISPLAY_PULL_TO_FILTER_TIP = "uihints.never_display_pull_to_filter_tip"; - private static final String HAS_SEEN_SCHEDULED_MESSAGES_INFO_ONCE = "uihints.has_seen_scheduled_messages_info_once"; - private static final String HAS_SEEN_TEXT_FORMATTING_ALERT = "uihints.text_formatting.has_seen_alert"; - private static final String HAS_NOT_SEEN_EDIT_MESSAGE_BETA_ALERT = "uihints.edit_message.has_not_seen_beta_alert"; - private static final String HAS_SEEN_SAFETY_NUMBER_NUX = "uihints.has_seen_safety_number_nux"; - private static final String DECLINED_NOTIFICATION_LOGS_PROMPT = "uihints.declined_notification_logs"; - private static final String LAST_NOTIFICATION_LOGS_PROMPT_TIME = "uihints.last_notification_logs_prompt"; - private static final String DISMISSED_BATTERY_SAVER_PROMPT = "uihints.declined_battery_saver_prompt"; - private static final String LAST_BATTERY_SAVER_PROMPT = "uihints.last_battery_saver_prompt"; - private static final String LAST_CRASH_PROMPT = "uihints.last_crash_prompt"; - private static final String HAS_COMPLETED_USERNAME_ONBOARDING = "uihints.has_completed_username_onboarding"; + private static final String HAS_SEEN_GROUP_SETTINGS_MENU_TOAST = "uihints.has_seen_group_settings_menu_toast"; + private static final String HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE = "uihints.has_confirmed_delete_for_everyone_once"; + private static final String HAS_SET_OR_SKIPPED_USERNAME_CREATION = "uihints.has_set_or_skipped_username_creation"; + private static final String NEVER_DISPLAY_PULL_TO_FILTER_TIP = "uihints.never_display_pull_to_filter_tip"; + private static final String HAS_SEEN_SCHEDULED_MESSAGES_INFO_ONCE = "uihints.has_seen_scheduled_messages_info_once"; + private static final String HAS_SEEN_TEXT_FORMATTING_ALERT = "uihints.text_formatting.has_seen_alert"; + private static final String HAS_NOT_SEEN_EDIT_MESSAGE_BETA_ALERT = "uihints.edit_message.has_not_seen_beta_alert"; + private static final String HAS_SEEN_SAFETY_NUMBER_NUX = "uihints.has_seen_safety_number_nux"; + private static final String DECLINED_NOTIFICATION_LOGS_PROMPT = "uihints.declined_notification_logs"; + private static final String LAST_NOTIFICATION_LOGS_PROMPT_TIME = "uihints.last_notification_logs_prompt"; + private static final String DISMISSED_BATTERY_SAVER_PROMPT = "uihints.declined_battery_saver_prompt"; + private static final String LAST_BATTERY_SAVER_PROMPT = "uihints.last_battery_saver_prompt"; + private static final String LAST_CRASH_PROMPT = "uihints.last_crash_prompt"; + private static final String HAS_COMPLETED_USERNAME_ONBOARDING = "uihints.has_completed_username_onboarding"; + private static final String HAS_SEEN_DOUBLE_TAP_EDIT_EDUCATION_SHEET = "uihints.has_seen_double_tap_edit_education_sheet"; UiHints(@NonNull KeyValueStore store) { super(store); @@ -158,4 +159,12 @@ public class UiHints extends SignalStoreValues { public long getLastCrashPrompt() { return getLong(LAST_CRASH_PROMPT, 0); } + + public void setHasSeenDoubleTapEditEducationSheet(boolean seen) { + putBoolean(HAS_SEEN_DOUBLE_TAP_EDIT_EDUCATION_SHEET, seen); + } + + public boolean getHasSeenDoubleTapEditEducationSheet() { + return getBoolean(HAS_SEEN_DOUBLE_TAP_EDIT_EDUCATION_SHEET, false); + } } diff --git a/app/src/main/res/layout/conversation_item_double_tap_edit_education_sheet.xml b/app/src/main/res/layout/conversation_item_double_tap_edit_education_sheet.xml new file mode 100644 index 0000000000..5ee2d608be --- /dev/null +++ b/app/src/main/res/layout/conversation_item_double_tap_edit_education_sheet.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35987d413f..2fdedd61f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3691,6 +3691,14 @@ Chat Broadcast + + + Double tap to edit + + Quickly tap twice on your messages to edit them. You can edit your messages up to 24hrs after they’ve been sent. + + Got it + New group Settings