Add double tap editing feature.

This commit is contained in:
mtang-signal 2024-04-25 17:24:21 -04:00 committed by Greyson Parrelli
parent 84e654efb2
commit ffc1463cda
13 changed files with 220 additions and 15 deletions

View file

@ -328,5 +328,7 @@ class V2ConversationItemShapeTest {
override fun onReportSpamLearnMoreClicked() = Unit
override fun onMessageRequestAcceptOptionsClicked() = Unit
override fun onItemDoubleClick(item: MultiselectPart) = Unit
}
}

View file

@ -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()
}

View file

@ -126,5 +126,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();
void onItemDoubleClick(MultiselectPart multiselectPart);
}
}

View file

@ -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<Slide> slides) {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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<MaterialButton>(R.id.got_it).setOnClickListener {
requireListener<Callback>().onDoubleTapEditEducationSheetNext(item.conversationMessage)
dismissAllowingStateLoss()
}
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
requireListener<Callback>().onDoubleTapEditEducationSheetNext(item.conversationMessage)
}
interface Callback {
fun onDoubleTapEditEducationSheetNext(conversationMessage: ConversationMessage)
}
}

View file

@ -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<Model : MappingModel<Model>>(
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<Model : MappingModel<Model>>(
)
}
binding.body.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
binding.root.setOnClickListener { onBubbleClicked() }
binding.root.setOnLongClickListener {
conversationContext.clickListener.onItemLongClick(binding.root, getMultiselectPartForLatestTouch())

View file

@ -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);
}
}

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:viewBindingIgnore="true">
<ImageView
android:id="@+id/handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/bottom_sheet_handle" />
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/handle"
app:tint="@color/signal_icon_tint_primary"
app:srcCompat="@drawable/ic_tap_outline_24" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="24dp"
android:text="@string/DoubleTapEditEducationSheet__double_tap_edit_title"
android:textAppearance="@style/Signal.Text.TitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/image" />
<TextView
android:id="@+id/double_tap_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginHorizontal="30dp"
android:gravity="center"
android:text="@string/DoubleTapEditEducationSheet__quickly_tap_twice"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<com.google.android.material.button.MaterialButton
android:id="@+id/got_it"
style="@style/Signal.Widget.Button.Medium.Primary"
android:backgroundTint="@color/signal_colorPrimaryContainer"
android:textColor="@color/signal_colorOnPrimaryContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:text="@string/DoubleTapEditEducationSheet__got_it"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/double_tap_details" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3691,6 +3691,14 @@
<string name="conversation_group_options__conversation">Chat</string>
<string name="conversation_group_options__broadcast">Broadcast</string>
<!-- DoubleTapEditEducationSheet -->
<!-- Displayed as the title of the education bottom sheet -->
<string name="DoubleTapEditEducationSheet__double_tap_edit_title">Double tap to edit</string>
<!-- Text on the sheet explaining how double tapping on a message will let them edit it -->
<string name="DoubleTapEditEducationSheet__quickly_tap_twice">Quickly tap twice on your messages to edit them. You can edit your messages up to 24hrs after theyve been sent.</string>
<!-- Button label to dismiss sheet -->
<string name="DoubleTapEditEducationSheet__got_it">Got it</string>
<!-- text_secure_normal -->
<string name="text_secure_normal__menu_new_group">New group</string>
<string name="text_secure_normal__menu_settings">Settings</string>