diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java index 1f80b108f0..171f199504 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; -import android.graphics.Typeface; import android.os.Build; import android.os.Bundle; import android.text.Annotation; @@ -17,9 +16,6 @@ import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.text.style.CharacterStyle; import android.text.style.RelativeSizeSpan; -import android.text.style.StrikethroughSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; import android.util.AttributeSet; import android.view.ActionMode; import android.view.Menu; @@ -63,7 +59,6 @@ import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER; public class ComposeText extends EmojiEditText { private static final char EMOJI_STARTER = ':'; - private static final long EMOJI_KEYWORD_DELAY = 1500; private static final Pattern TIME_PATTERN = Pattern.compile("^[0-9]{1,2}:[0-9]{1,2}$"); @@ -76,13 +71,6 @@ public class ComposeText extends EmojiEditText { @Nullable private CursorPositionChangedListener cursorPositionChangedListener; @Nullable private InlineQueryChangedListener inlineQueryChangedListener; - private final Runnable keywordSearchRunnable = () -> { - Editable text = getText(); - if (text != null && enoughToFilter(text, true)) { - performFiltering(text, true); - } - }; - public ComposeText(Context context) { super(context); initialize(); @@ -362,7 +350,7 @@ public class ComposeText extends EmojiEditText { } if (style != null) { - replacement.setSpan(style, 0, charSequence.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + replacement.setSpan(style, 0, charSequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } clearComposingText(); @@ -532,6 +520,11 @@ public class ComposeText extends EmojiEditText { return -1; } + @Override + protected boolean shouldPersistSignalStylingWhenPasting() { + return true; + } + /** * Return true if we think the user may be inputting a time. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java index ab234b81e3..e78cf9e0ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java @@ -1,9 +1,13 @@ package org.thoughtcrime.securesms.components.emoji; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.os.Build; import android.text.InputFilter; +import android.text.TextUtils; import android.util.AttributeSet; import androidx.annotation.NonNull; @@ -14,7 +18,9 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.EditTextExtensionsKt; +import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import java.util.HashSet; import java.util.Set; @@ -35,9 +41,9 @@ public class EmojiEditText extends AppCompatEditText { public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0); - boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false); - boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false); + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0); + boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false); + boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false); a.recycle(); if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) { @@ -57,8 +63,8 @@ public class EmojiEditText extends AppCompatEditText { } public void insertEmoji(String emoji) { - final int start = getSelectionStart(); - final int end = getSelectionEnd(); + final int start = getSelectionStart(); + final int end = getSelectionEnd(); getText().replace(Math.min(start, end), Math.max(start, end), emoji); setSelection(start + emoji.length()); @@ -66,8 +72,11 @@ public class EmojiEditText extends AppCompatEditText { @Override public void invalidateDrawable(@NonNull Drawable drawable) { - if (drawable instanceof EmojiDrawable) invalidate(); - else super.invalidateDrawable(drawable); + if (drawable instanceof EmojiDrawable) { + invalidate(); + } else { + super.invalidateDrawable(drawable); + } } @Override @@ -95,4 +104,50 @@ public class EmojiEditText extends AppCompatEditText { return result; } + + @Override + public boolean onTextContextMenuItem(int id) { + if (id == android.R.id.paste) { + ClipData clipData = ServiceUtil.getClipboardManager(getContext()).getPrimaryClip(); + + if (clipData != null) { + CharSequence label = clipData.getDescription().getLabel(); + CharSequence pendingPaste = getTextFromClipData(clipData); + + if (TextUtils.equals(Util.COPY_LABEL, label) && shouldPersistSignalStylingWhenPasting()) { + return super.onTextContextMenuItem(id); + } else if (Build.VERSION.SDK_INT >= 23) { + return super.onTextContextMenuItem(android.R.id.pasteAsPlainText); + } else if (pendingPaste != null) { + Util.copyToClipboard(getContext(), pendingPaste.toString()); + return super.onTextContextMenuItem(id); + } + } + } else if (id == android.R.id.copy || id == android.R.id.cut) { + boolean originalResult = super.onTextContextMenuItem(id); + ClipboardManager clipboardManager = ServiceUtil.getClipboardManager(getContext()); + CharSequence clipText = getTextFromClipData(clipboardManager.getPrimaryClip()); + + if (clipText != null) { + Util.copyToClipboard(getContext(), clipText); + return true; + } + + return originalResult; + } + + return super.onTextContextMenuItem(id); + } + + private @Nullable CharSequence getTextFromClipData(@Nullable ClipData data) { + if (data != null && data.getItemCount() > 0) { + return data.getItemAt(0).coerceToText(getContext()); + } else { + return null; + } + } + + protected boolean shouldPersistSignalStylingWhenPasting() { + return false; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 40a1deb37f..09ead35f5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -18,7 +18,6 @@ package org.thoughtcrime.securesms.conversation; import android.Manifest; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.app.Activity; import android.app.PendingIntent; import android.content.ActivityNotFoundException; @@ -284,6 +283,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.ContextUtil; import org.thoughtcrime.securesms.util.ConversationUtil; import org.thoughtcrime.securesms.util.Debouncer; +import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DrawableUtil; import org.thoughtcrime.securesms.util.FullscreenHelper; import org.thoughtcrime.securesms.util.IdentityUtil; @@ -3015,6 +3015,11 @@ public class ConversationParentFragment extends Fragment return; } + if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && result.getBodyRanges() != null && result.getBodyRanges().getRangesCount() > 0) { + Dialogs.showFormattedTextDialog(requireContext(), () -> sendMediaMessage(result)); + return; + } + long thread = this.threadId; long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds()); QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null); @@ -3127,6 +3132,12 @@ public class ConversationParentFragment extends Fragment return new SettableFuture<>(null); } + if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && styling != null && styling.getRangesCount() > 0) { + final String finalBody = body; + Dialogs.showFormattedTextDialog(requireContext(), () -> sendMediaMessage(recipientId, sendType, finalBody, slideDeck, quote, contacts, previews, mentions, styling, expiresIn, viewOnce, initiating, clearComposeBox, metricId, scheduledDate)); + return new SettableFuture<>(null); + } + final boolean sendPush = sendType.usesSignalTransport(); final long thread = this.threadId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageStyler.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageStyler.kt index 0a6853e339..3bc866e17d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageStyler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageStyler.kt @@ -8,7 +8,6 @@ import android.text.style.StrikethroughSpan import android.text.style.StyleSpan import android.text.style.TypefaceSpan import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList -import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.PlaceholderURLSpan /** @@ -62,7 +61,7 @@ object MessageStyler { } if (styleSpan != null) { - span.setSpan(styleSpan, range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_INCLUSIVE) + span.setSpan(styleSpan, range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) appliedStyle = true } } else if (range.hasLink() && range.link != null) { @@ -82,17 +81,14 @@ object MessageStyler { @JvmStatic fun hasStyling(text: Spanned): Boolean { - return if (FeatureFlags.textFormatting()) { - text.getSpans(0, text.length, CharacterStyle::class.java) - .any { s -> isSupportedCharacterStyle(s) && text.getSpanEnd(s) - text.getSpanStart(s) > 0 } - } else { - false - } + return text + .getSpans(0, text.length, CharacterStyle::class.java) + .any { s -> isSupportedCharacterStyle(s) && text.getSpanEnd(s) - text.getSpanStart(s) > 0 } } @JvmStatic fun getStyling(text: CharSequence?): BodyRangeList? { - val bodyRanges = if (text is Spanned && FeatureFlags.textFormatting()) { + val bodyRanges = if (text is Spanned) { text .getSpans(0, text.length, CharacterStyle::class.java) .filter { s -> isSupportedCharacterStyle(s) } @@ -125,7 +121,7 @@ object MessageStyler { } } - private fun isSupportedCharacterStyle(style: CharacterStyle): Boolean { + fun isSupportedCharacterStyle(style: CharacterStyle): Boolean { return when (style) { is StyleSpan -> style.style == Typeface.ITALIC || style.style == Typeface.BOLD is StrikethroughSpan -> true diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt index 87bc6a27a7..e93366b33b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt @@ -4,7 +4,6 @@ package org.thoughtcrime.securesms.database.model import com.google.protobuf.ByteString import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList -import org.thoughtcrime.securesms.util.FeatureFlags import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange /** @@ -49,7 +48,7 @@ fun BodyRangeList.Builder.addButton(label: String, action: String, start: Int, l } fun List?.toBodyRangeList(): BodyRangeList? { - if (this == null || !FeatureFlags.textFormatting()) { + if (this == null) { return null } 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 7471cab92c..54da25865d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java @@ -15,6 +15,8 @@ public class UiHints extends SignalStoreValues { 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_USERNAME_EDUCATION = "uihints.has_seen_username_education"; + private static final String HAS_SEEN_TEXT_FORMATTING_ALERT = "uihints.text_formatting.has_seen_alert"; + UiHints(@NonNull KeyValueStore store) { super(store); } @@ -26,7 +28,7 @@ public class UiHints extends SignalStoreValues { @Override @NonNull List getKeysToIncludeInBackup() { - return Arrays.asList(NEVER_DISPLAY_PULL_TO_FILTER_TIP, HAS_SEEN_USERNAME_EDUCATION); + return Arrays.asList(NEVER_DISPLAY_PULL_TO_FILTER_TIP, HAS_SEEN_USERNAME_EDUCATION, HAS_SEEN_TEXT_FORMATTING_ALERT); } public void markHasSeenGroupSettingsMenuToast() { @@ -90,4 +92,12 @@ public class UiHints extends SignalStoreValues { private int getNeverDisplayPullToFilterTip() { return getInteger(NEVER_DISPLAY_PULL_TO_FILTER_TIP, 0); } + + public boolean hasNotSeenTextFormattingAlert() { + return getBoolean(HAS_SEEN_TEXT_FORMATTING_ALERT, true); + } + + public void markHasSeenTextFormattingAlert() { + putBoolean(HAS_SEEN_TEXT_FORMATTING_ALERT, false); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyDialogFragment.kt index 766d47fa7b..39e9ca1155 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyDialogFragment.kt @@ -18,10 +18,12 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageViewModel import org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReplyComposer +import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.ViewUtil @@ -71,13 +73,22 @@ class StoryDirectReplyDialogFragment : composer = view.findViewById(R.id.input) composer.callback = object : StoryReplyComposer.Callback { override fun onSendActionClicked() { - val (body, _, bodyRanges) = composer.consumeInput() - lifecycleDisposable += viewModel.sendReply(body, bodyRanges) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - Toast.makeText(requireContext(), R.string.StoryDirectReplyDialogFragment__sending_reply, Toast.LENGTH_LONG).show() - dismissAllowingStateLoss() - } + val sendReply = Runnable { + val (body, _, bodyRanges) = composer.consumeInput() + + lifecycleDisposable += viewModel.sendReply(body, bodyRanges) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + Toast.makeText(requireContext(), R.string.StoryDirectReplyDialogFragment__sending_reply, Toast.LENGTH_LONG).show() + dismissAllowingStateLoss() + } + } + + if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && composer.input.hasStyling()) { + Dialogs.showFormattedTextDialog(requireContext(), sendReply) + } else { + sendReply.run() + } } override fun onReactionClicked(emoji: String) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt index 0948ba27f5..d4c5016a69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileJob import org.thoughtcrime.securesms.keyboard.KeyboardPage import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardCallback +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment @@ -56,6 +57,7 @@ import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPager import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerParent import org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReplyComposer import org.thoughtcrime.securesms.util.DeleteDialog +import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -350,8 +352,16 @@ class StoryGroupReplyFragment : } override fun onSendActionClicked() { - val (body, mentions, bodyRanges) = composer.consumeInput() - performSend(body, mentions, bodyRanges) + val send = Runnable { + val (body, mentions, bodyRanges) = composer.consumeInput() + performSend(body, mentions, bodyRanges) + } + + if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && composer.input.hasStyling()) { + Dialogs.showFormattedTextDialog(requireContext(), send) + } else { + send.run() + } } override fun onPickAnyReactionClicked() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java index a63547e9fe..d486ccb945 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java @@ -18,9 +18,12 @@ package org.thoughtcrime.securesms.util; import android.content.Context; +import androidx.annotation.NonNull; + import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; public class Dialogs { public static void showAlertDialog(Context context, String title, String message) { @@ -39,4 +42,16 @@ public class Dialogs { .setPositiveButton(android.R.string.ok, null) .show(); } + + public static void showFormattedTextDialog(@NonNull Context context, @NonNull Runnable onSendAnyway) { + new MaterialAlertDialogBuilder(context) + .setTitle(R.string.SendingFormattingTextDialog_title) + .setMessage(R.string.SendingFormattingTextDialog_message) + .setNegativeButton(R.string.SendingFormattingTextDialog_cancel_send_button, null) + .setPositiveButton(R.string.SendingFormattingTextDialog_send_anyway_button, (d, w) -> { + SignalStore.uiHints().markHasSeenTextFormattingAlert(); + onSendAnyway.run(); + }) + .show(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 0b5c43f751..2e15b73aa4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -71,6 +71,8 @@ public class Util { private static final long BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90); + public static final String COPY_LABEL = "text\u00AD"; + public static List asList(T... elements) { List result = new LinkedList<>(); Collections.addAll(result, elements); @@ -482,7 +484,7 @@ public class Util { } public static void copyToClipboard(@NonNull Context context, @NonNull CharSequence text) { - ServiceUtil.getClipboardManager(context).setPrimaryClip(ClipData.newPlainText("text", text)); + ServiceUtil.getClipboardManager(context).setPrimaryClip(ClipData.newPlainText(COPY_LABEL, text)); } @SafeVarargs diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cab2b0921c..b6fc89e7f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -384,6 +384,15 @@ You will be reminded again soon. + + Sending formatted text + + Some people may be using a version of Signal that doesn\'t support formatted text. They will not be able to see the formatting changes you\'ve made to your message. + + Send anyway + + Cancel + %d unread message