mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 00:29:11 +01:00
Add additional text formatting support.
This commit is contained in:
committed by
Greyson Parrelli
parent
1c3636eedd
commit
25028e0e6f
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<BodyRange>?.toBodyRangeList(): BodyRangeList? {
|
||||
if (this == null || !FeatureFlags.textFormatting()) {
|
||||
if (this == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T> List<T> asList(T... elements) {
|
||||
List<T> 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
|
||||
|
||||
Reference in New Issue
Block a user