mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 19:56:02 +01:00
Fix rendering of links and mentions covered by spoilers.
This commit is contained in:
@@ -363,7 +363,7 @@ public class ComposeText extends EmojiEditText {
|
||||
} else if (item.getItemId() == R.id.edittext_monospace) {
|
||||
style = MessageStyler.monoStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_spoiler) {
|
||||
style = MessageStyler.spoilerStyle(start, charSequence.length(), text);
|
||||
style = MessageStyler.spoilerStyle(MessageStyler.COMPOSE_ID, start, charSequence.length(), text);
|
||||
}
|
||||
|
||||
if (style != null) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -27,8 +28,10 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.quotes.QuoteViewColorTheme;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
@@ -82,7 +85,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private ViewGroup mainView;
|
||||
private ViewGroup footerView;
|
||||
private TextView authorView;
|
||||
private TextView bodyView;
|
||||
private EmojiTextView bodyView;
|
||||
private View quoteBarView;
|
||||
private ShapeableImageView thumbnailView;
|
||||
private View attachmentVideoOverlayView;
|
||||
@@ -163,6 +166,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
setMessageType(messageType);
|
||||
|
||||
bodyView.enableSpoilerFiltering();
|
||||
dismissView.setOnClickListener(view -> setVisibility(GONE));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Annotation;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextDirectionHeuristic;
|
||||
@@ -26,7 +27,6 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRendererDelegate;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.SpoilerFilteringSpannable;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -67,9 +68,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private TextDirectionHeuristic textDirection;
|
||||
private boolean isJumbomoji;
|
||||
private boolean forceJumboEmoji;
|
||||
private boolean isInOnDraw;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private final SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private final SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
private SpoilerFilteringSpannableFactory spoilerFilteringSpannableFactory;
|
||||
|
||||
public EmojiTextView(Context context) {
|
||||
this(context, null);
|
||||
@@ -105,8 +108,14 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
setEmojiCompatEnabled(useSystemEmoji());
|
||||
}
|
||||
|
||||
public void enableSpoilerFiltering() {
|
||||
spoilerFilteringSpannableFactory = new SpoilerFilteringSpannableFactory();
|
||||
setSpannableFactory(spoilerFilteringSpannableFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
isInOnDraw = true;
|
||||
if (getText() instanceof Spanned && getLayout() != null) {
|
||||
int checkpoint = canvas.save();
|
||||
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||
@@ -120,6 +129,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
isInOnDraw = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,13 +161,18 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
previousTransformationMethod = getTransformationMethod();
|
||||
|
||||
Spannable textToSet;
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(new SpannableStringBuilder(Optional.ofNullable(text).orElse("")), BufferType.SPANNABLE);
|
||||
textToSet = new SpannableStringBuilder(Optional.ofNullable(text).orElse(""));
|
||||
} else {
|
||||
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji);
|
||||
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
|
||||
textToSet = new SpannableStringBuilder(EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji));
|
||||
}
|
||||
|
||||
if (spoilerFilteringSpannableFactory != null) {
|
||||
textToSet = spoilerFilteringSpannableFactory.wrap(textToSet);
|
||||
}
|
||||
super.setText(textToSet, BufferType.SPANNABLE);
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
|
||||
@@ -410,4 +425,15 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
mentionRendererDelegate.setTint(mentionBackgroundTint);
|
||||
}
|
||||
}
|
||||
|
||||
private class SpoilerFilteringSpannableFactory extends Spannable.Factory {
|
||||
@Override
|
||||
public @NonNull Spannable newSpannable(CharSequence source) {
|
||||
return wrap(super.newSpannable(source));
|
||||
}
|
||||
|
||||
@NonNull SpoilerFilteringSpannable wrap(Spannable source) {
|
||||
return new SpoilerFilteringSpannable(source, () -> isInOnDraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,27 +22,23 @@ object SpoilerAnnotation {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isSpoilerAnnotation(annotation: Annotation): Boolean {
|
||||
return SPOILER_ANNOTATION == annotation.key
|
||||
fun isSpoilerAnnotation(annotation: Any): Boolean {
|
||||
return SPOILER_ANNOTATION == (annotation as? Annotation)?.key
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getSpoilerAnnotations(spanned: Spanned): List<Annotation> {
|
||||
val spoilerAnnotations: Map<Pair<Int, Int>, Annotation> = spanned.getSpans(0, spanned.length, Annotation::class.java)
|
||||
fun getSpoilerAndClickAnnotations(spanned: Spanned, start: Int = 0, end: Int = spanned.length): Map<Annotation, SpoilerClickableSpan?> {
|
||||
val spoilerAnnotations: Map<Pair<Int, Int>, Annotation> = spanned.getSpans(start, end, Annotation::class.java)
|
||||
.filter { isSpoilerAnnotation(it) }
|
||||
.associateBy { (spanned.getSpanStart(it) to spanned.getSpanEnd(it)) }
|
||||
|
||||
val spoilerClickSpans: Map<Pair<Int, Int>, SpoilerClickableSpan> = spanned.getSpans(0, spanned.length, SpoilerClickableSpan::class.java)
|
||||
val spoilerClickSpans: Map<Pair<Int, Int>, SpoilerClickableSpan> = spanned.getSpans(start, end, SpoilerClickableSpan::class.java)
|
||||
.associateBy { (spanned.getSpanStart(it) to spanned.getSpanEnd(it)) }
|
||||
|
||||
return spoilerAnnotations.mapNotNull { (position, annotation) ->
|
||||
if (spoilerClickSpans[position]?.spoilerRevealed != true && !revealedSpoilers.contains(annotation.value)) {
|
||||
annotation
|
||||
} else {
|
||||
revealedSpoilers.add(annotation.value)
|
||||
null
|
||||
return spoilerAnnotations
|
||||
.map { (position, annotation) ->
|
||||
annotation to spoilerClickSpans[position]
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -57,18 +53,12 @@ object SpoilerAnnotation {
|
||||
revealedSpoilers.clear()
|
||||
}
|
||||
|
||||
class SpoilerClickableSpan(spoiler: Annotation) : ClickableSpan() {
|
||||
private val spoiler: Annotation
|
||||
var spoilerRevealed = false
|
||||
private set
|
||||
|
||||
init {
|
||||
this.spoiler = spoiler
|
||||
spoilerRevealed = revealedSpoilers.contains(spoiler.value)
|
||||
}
|
||||
class SpoilerClickableSpan(private val spoiler: Annotation) : ClickableSpan() {
|
||||
val spoilerRevealed
|
||||
get() = revealedSpoilers.contains(spoiler.value)
|
||||
|
||||
override fun onClick(widget: View) {
|
||||
spoilerRevealed = true
|
||||
revealedSpoilers.add(spoiler.value)
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.text.Layout
|
||||
import android.text.Spanned
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation.SpoilerClickableSpan
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRenderer.MultiLineSpoilerRenderer
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRenderer.SingleLineSpoilerRenderer
|
||||
|
||||
@@ -26,7 +27,7 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
private var spoilerDrawablePool = mutableMapOf<Annotation, List<SpoilerDrawable>>()
|
||||
private var nextSpoilerDrawablePool = mutableMapOf<Annotation, List<SpoilerDrawable>>()
|
||||
|
||||
private val cachedAnnotations = HashMap<Int, List<Annotation>>()
|
||||
private val cachedAnnotations = HashMap<Int, Map<Annotation, SpoilerClickableSpan?>>()
|
||||
private val cachedMeasurements = HashMap<Int, SpanMeasurements>()
|
||||
|
||||
private val animator = ValueAnimator.ofInt(0, 100).apply {
|
||||
@@ -56,10 +57,14 @@ class SpoilerRendererDelegate @JvmOverloads constructor(private val view: TextVi
|
||||
|
||||
fun draw(canvas: Canvas, text: Spanned, layout: Layout) {
|
||||
var hasSpoilersToRender = false
|
||||
val annotations: List<Annotation> = cachedAnnotations.getFromCache(text) { SpoilerAnnotation.getSpoilerAnnotations(text) }
|
||||
val annotations: Map<Annotation, SpoilerClickableSpan?> = cachedAnnotations.getFromCache(text) { SpoilerAnnotation.getSpoilerAndClickAnnotations(text) }
|
||||
|
||||
nextSpoilerDrawablePool.clear()
|
||||
for (annotation in annotations) {
|
||||
for ((annotation, clickSpan) in annotations.entries) {
|
||||
if (clickSpan?.spoilerRevealed == true) {
|
||||
continue
|
||||
}
|
||||
|
||||
val spanStart: Int = text.getSpanStart(annotation)
|
||||
val spanEnd: Int = text.getSpanEnd(annotation)
|
||||
if (spanStart >= spanEnd) {
|
||||
|
||||
Reference in New Issue
Block a user