Skip to content

Commit

Permalink
Fix rendering of links and mentions covered by spoilers.
Browse files Browse the repository at this point in the history
  • Loading branch information
cody-signal committed Mar 24, 2023
1 parent 168e37c commit 7eb00e4
Show file tree
Hide file tree
Showing 15 changed files with 154 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
} 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;
Expand All @@ -27,8 +28,10 @@
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;
Expand Down Expand Up @@ -82,7 +85,7 @@ public enum MessageType {
private ViewGroup mainView;
private ViewGroup footerView;
private TextView authorView;
private TextView bodyView;
private EmojiTextView bodyView;
private View quoteBarView;
private ShapeableImageView thumbnailView;
private View attachmentVideoOverlayView;
Expand Down Expand Up @@ -163,6 +166,7 @@ private void initialize(@Nullable AttributeSet attrs) {

setMessageType(messageType);

bodyView.enableSpoilerFiltering();
dismissView.setOnClickListener(view -> setVisibility(GONE));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand All @@ -26,7 +27,6 @@
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;
Expand All @@ -35,6 +35,7 @@
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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -105,8 +108,14 @@ public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
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());
Expand All @@ -120,6 +129,7 @@ protected void onDraw(Canvas canvas) {
}
}
super.onDraw(canvas);
isInOnDraw = false;
}

@Override
Expand Down Expand Up @@ -151,13 +161,18 @@ public void setText(@Nullable CharSequence text, BufferType type) {
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()) {
Expand Down Expand Up @@ -410,4 +425,15 @@ public void setMentionBackgroundTint(@ColorInt int mentionBackgroundTint) {
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ protected void onFinishInflate() {

bodyText.setOnLongClickListener(passthroughClickListener);
bodyText.setOnClickListener(passthroughClickListener);
bodyText.enableSpoilerFiltering();
footer.setOnTouchDelegateChangedListener(touchDelegateChangedListener);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public static class ConversationMessageFactory {
: BodyRangeUtil.adjustBodyRanges(messageRecord.getMessageRanges(), mentionsUpdate.getBodyAdjustments());

styledAndMentionBody = SpannableString.valueOf(mentionsUpdate != null ? mentionsUpdate.getBody() : body);
styleResult = MessageStyler.style(bodyRanges, styledAndMentionBody);
styleResult = MessageStyler.style(messageRecord.getId(), bodyRanges, styledAndMentionBody);
}

return new ConversationMessage(messageRecord,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ object MessageStyler {

const val MONOSPACE = "monospace"
const val SPAN_FLAGS = Spanned.SPAN_EXCLUSIVE_INCLUSIVE
const val DRAFT_ID = "DRAFT"
const val COMPOSE_ID = "COMPOSE"
const val QUOTE_ID = "QUOTE"

@JvmStatic
fun boldStyle(): CharacterStyle {
Expand All @@ -41,13 +44,13 @@ object MessageStyler {
}

@JvmStatic
fun spoilerStyle(start: Int, length: Int, body: Spannable? = null): Annotation {
return SpoilerAnnotation.spoilerAnnotation(arrayOf(start, length, body?.toString()).contentHashCode())
fun spoilerStyle(id: Any, start: Int, length: Int, body: Spannable? = null): Annotation {
return SpoilerAnnotation.spoilerAnnotation(arrayOf(id, start, length, body?.toString()).contentHashCode())
}

@JvmStatic
@JvmOverloads
fun style(messageRanges: BodyRangeList?, span: Spannable, hideSpoilerText: Boolean = true): Result {
fun style(id: Any, messageRanges: BodyRangeList?, span: Spannable, hideSpoilerText: Boolean = true): Result {
if (messageRanges == null) {
return Result.none()
}
Expand All @@ -67,7 +70,7 @@ object MessageStyler {
BodyRangeList.BodyRange.Style.STRIKETHROUGH -> strikethroughStyle()
BodyRangeList.BodyRange.Style.MONOSPACE -> monoStyle()
BodyRangeList.BodyRange.Style.SPOILER -> {
val spoiler = spoilerStyle(range.start, range.length, span)
val spoiler = spoilerStyle(id, range.start, range.length, span)
if (hideSpoilerText) {
span.setSpan(SpoilerAnnotation.SpoilerClickableSpan(spoiler), range.start, range.start + range.length, SPAN_FLAGS)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class DraftRepository(

updatedText = SpannableString(updated.body)
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
MessageStyler.style(messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = updatedText, hideSpoilerText = false)
MessageStyler.style(id = MessageStyler.DRAFT_ID, messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = updatedText, hideSpoilerText = false)
}

DatabaseDraft(drafts, updatedText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ private void onRecipientChanged(@NonNull Recipient recipient) {
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
} else {
SpannableStringBuilder sourceBody = new SpannableStringBuilder(thread.getBody());
MessageStyler.style(thread.getBodyRanges(), sourceBody);
MessageStyler.style(thread.getDate(), thread.getBodyRanges(), sourceBody);

CharSequence body = StringUtil.replace(sourceBody, '\n', " ");
LiveData<SpannableString> finalBody = Transformations.map(createFinalBodyWithMediaIcon(context, body, thread, glideRequests, thumbSize, thumbTarget), updatedBody -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.DatabaseAttachment.DisplayOrderComparator
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.conversation.MessageStyler.style
import org.thoughtcrime.securesms.conversation.MessageStyler
import org.thoughtcrime.securesms.database.EarlyReceiptCache.Receipt
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
Expand Down Expand Up @@ -5109,7 +5109,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val updated: UpdatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions)
val styledText = SpannableString(updated.body)

style(bodyRanges.adjustBodyRanges(updated.bodyAdjustments), styledText)
MessageStyler.style(id = quoteId, messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = styledText)

quoteText = styledText
quoteMentions = updated.mentions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,11 @@ private List<ThreadRecord> getMatchingThreads(@NonNull Collection<RecipientId> r

if (ranges != null) {
updatedBody = SpannableString.valueOf(updatedBody);
MessageStyler.style(BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);
MessageStyler.style(result.getMessageId(), BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);

updatedSnippet = SpannableString.valueOf(updatedSnippet);
//noinspection ConstantConditions
updateSnippetWithStyles(updatedBody, (SpannableString) updatedSnippet, BodyRangeUtil.adjustBodyRanges(ranges, snippetAdjustments));
updateSnippetWithStyles(result.getMessageId(), updatedBody, (SpannableString) updatedSnippet, BodyRangeUtil.adjustBodyRanges(ranges, snippetAdjustments));
}

updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), updatedBody, updatedSnippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
Expand Down Expand Up @@ -302,7 +302,7 @@ private List<ThreadRecord> getMatchingThreads(@NonNull Collection<RecipientId> r
}
}

private void updateSnippetWithStyles(@NonNull CharSequence body, @NonNull SpannableString bodySnippet, @NonNull BodyRangeList bodyRanges) {
private void updateSnippetWithStyles(long id, @NonNull CharSequence body, @NonNull SpannableString bodySnippet, @NonNull BodyRangeList bodyRanges) {
CharSequence cleanSnippet = bodySnippet;
int startOffset = 0;

Expand All @@ -326,7 +326,7 @@ private void updateSnippetWithStyles(@NonNull CharSequence body, @NonNull Spanna
}
}

MessageStyler.style(builder.build(), bodySnippet);
MessageStyler.style(id, builder.build(), bodySnippet);
}
}

Expand Down Expand Up @@ -361,7 +361,7 @@ private void updateSnippetWithStyles(@NonNull CharSequence body, @NonNull Spanna
SpannableString body = new SpannableString(record.getBody());

if (bodyRanges != null) {
MessageStyler.style(bodyRanges, body);
MessageStyler.style(record.getId(), bodyRanges, body);
}

CharSequence updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions).getBody();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class StoryTextPostView @JvmOverloads constructor(
} else {
val body = SpannableString(storyTextPost.body)
if (font == TextFont.REGULAR && bodyRanges != null) {
MessageStyler.style(bodyRanges, body)
MessageStyler.style(System.currentTimeMillis(), bodyRanges, body)
}
setText(body, false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ class StoryViewerPageFragment :
val displayBodySpan = SpannableString(storyPost.content.attachment.caption ?: "")
val ranges: BodyRangeList? = storyPost.conversationMessage.messageRecord.messageRanges
if (ranges != null && displayBodySpan.isNotEmpty()) {
MessageStyler.style(ranges, displayBodySpan)
MessageStyler.style(storyPost.id, ranges, displayBodySpan)
}

displayBodySpan
Expand Down

0 comments on commit 7eb00e4

Please sign in to comment.