Skip to content

Commit

Permalink
Add double tap editing feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
mtang-signal authored and greyson-signal committed Apr 29, 2024
1 parent 84e654e commit ffc1463
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -328,5 +328,7 @@ class V2ConversationItemShapeTest {
override fun onReportSpamLearnMoreClicked() = Unit

override fun onMessageRequestAcceptOptionsClicked() = Unit

override fun onItemDoubleClick(item: MultiselectPart) = Unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,6 @@ interface EventListener {
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();
void onItemDoubleClick(MultiselectPart multiselectPart);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -351,6 +353,7 @@ protected void onFinishInflate() {

setOnClickListener(new ClickListener(null));

bodyText.setOnTouchListener(doubleTapEditTouchListener);
bodyText.setOnLongClickListener(passthroughClickListener);
bodyText.setOnClickListener(passthroughClickListener);
footer.setOnTouchDelegateChangedListener(touchDelegateChangedListener);
Expand Down Expand Up @@ -2438,6 +2441,24 @@ public void onClick(final View view) {
}
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -4307,4 +4322,8 @@ class ConversationFragment :
}
}
}

override fun onDoubleTapEditEducationSheetNext(conversationMessage: ConversationMessage) {
handleEditMessage(conversationMessage)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down
37 changes: 23 additions & 14 deletions app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -158,4 +159,12 @@ public void setLastCrashPrompt(long time) {
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);
}
}
Original file line number Diff line number Diff line change
@@ -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>
8 changes: 8 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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 they’ve 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>
Expand Down

0 comments on commit ffc1463

Please sign in to comment.