From b9d7d19deaf58a15dfbd8c7b0f2a0a21acac3335 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 25 Apr 2023 09:51:11 -0400 Subject: [PATCH] Fix multiple issues with rendering spoilers as story captions. --- .../components/emoji/EmojiTextView.java | 48 ++++++++++------- .../stories/viewer/StoryViewerFragment.kt | 3 ++ .../viewer/page/StoryViewerPageFragment.kt | 53 ++++++------------- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 47 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java index cd79e39ebb..90ee60130c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -320,23 +320,7 @@ public class EmojiTextView extends AppCompatTextView { if (maxLength > 0 && getText().length() > maxLength + 1) { SpannableStringBuilder newContent = new SpannableStringBuilder(); - SpannableString shortenedText = new SpannableString(getText().subSequence(0, maxLength)); - List mentionAnnotations = MentionAnnotation.getMentionAnnotations(shortenedText, maxLength - 1, maxLength); - if (!mentionAnnotations.isEmpty()) { - shortenedText = new SpannableString(shortenedText.subSequence(0, shortenedText.getSpanStart(mentionAnnotations.get(0)))); - } - - Object[] endSpans = shortenedText.getSpans(shortenedText.length() - 1, shortenedText.length(), Object.class); - for (Object span : endSpans) { - if (shortenedText.getSpanFlags(span) == Spanned.SPAN_EXCLUSIVE_INCLUSIVE) { - int start = shortenedText.getSpanStart(span); - int end = shortenedText.getSpanEnd(span); - shortenedText.removeSpan(span); - shortenedText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - newContent.append(shortenedText) + newContent.append(getText(maxLength)) .append(ELLIPSIS) .append(Util.emptyIfNull(overflowText)); @@ -379,9 +363,12 @@ public class EmojiTextView extends AppCompatTextView { CharSequence ellipsized = StringUtil.trim(TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END)); SpannableStringBuilder newContent = new SpannableStringBuilder(); - newContent.append(getText().subSequence(0, overflowStart)) - .append(ellipsized.subSequence(0, ellipsized.length())) - .append(Optional.ofNullable(overflowText).orElse("")); + newContent.append(getText().subSequence(0, overflowStart).toString()) + .append(ellipsized.subSequence(0, ellipsized.length()).toString()); + + TextUtils.copySpansFrom(getText(newContent.length() - 1), 0, newContent.length() - 1, Object.class, newContent, 0); + + newContent.append(Optional.ofNullable(overflowText).orElse("")); EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent); @@ -406,6 +393,27 @@ public class EmojiTextView extends AppCompatTextView { } } + /** Get text but truncated to maxLength, adjusts for end mentions and converts style spans to be exclusive on start and end. */ + private SpannableString getText(int maxLength) { + SpannableString shortenedText = new SpannableString(getText().subSequence(0, maxLength)); + List mentionAnnotations = MentionAnnotation.getMentionAnnotations(shortenedText, maxLength - 1, maxLength); + if (!mentionAnnotations.isEmpty()) { + shortenedText = new SpannableString(shortenedText.subSequence(0, shortenedText.getSpanStart(mentionAnnotations.get(0)))); + } + + Object[] endSpans = shortenedText.getSpans(shortenedText.length() - 1, shortenedText.length(), Object.class); + for (Object span : endSpans) { + if (shortenedText.getSpanFlags(span) == Spanned.SPAN_EXCLUSIVE_INCLUSIVE) { + int start = shortenedText.getSpanStart(span); + int end = shortenedText.getSpanEnd(span); + shortenedText.removeSpan(span); + shortenedText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + return shortenedText; + } + private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) { return Util.equals(previousText, text) && Util.equals(previousOverflowText, overflowText) && diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt index 434748d4c8..263d73a424 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt @@ -16,6 +16,7 @@ import org.signal.core.util.getParcelableArrayListCompat import org.signal.core.util.getParcelableCompat import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -58,6 +59,8 @@ class StoryViewerFragment : ViewCompat.setTransitionName(storyCrossfader, "story") storyCrossfader.callback = this + SpoilerAnnotation.resetRevealedSpoilers() + val adapter = StoryViewerPagerAdapter( this, StoryViewerPageArgs( diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 3fc887534e..284947c65d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -10,6 +10,7 @@ import android.graphics.drawable.Drawable import android.media.AudioManager import android.os.Bundle import android.text.SpannableString +import android.text.method.LinkMovementMethod import android.text.method.ScrollingMovementMethod import android.view.GestureDetector import android.view.MotionEvent @@ -24,7 +25,6 @@ import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.view.GestureDetectorCompat import androidx.core.view.animation.PathInterpolatorCompat -import androidx.core.view.doOnNextLayout import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -42,6 +42,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.animation.AnimationCompleteListener import org.thoughtcrime.securesms.components.AvatarImageView +import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.components.segmentedprogressbar.SegmentedProgressBar import org.thoughtcrime.securesms.components.segmentedprogressbar.SegmentedProgressBarListener import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto @@ -177,7 +178,7 @@ class StoryViewerPageFragment : val distributionList: TextView = view.findViewById(R.id.distribution_list) val cardWrapper: TouchInterceptingFrameLayout = view.findViewById(R.id.story_content_card_touch_interceptor) val card: MaterialCardView = view.findViewById(R.id.story_content_card) - val caption: TextView = view.findViewById(R.id.story_caption) + val caption: EmojiTextView = view.findViewById(R.id.story_caption) val largeCaption: TextView = view.findViewById(R.id.story_large_caption) val largeCaptionOverlay: View = view.findViewById(R.id.story_large_caption_overlay) val reactionAnimationView: OnReactionSentView = view.findViewById(R.id.on_reaction_sent_view) @@ -817,7 +818,7 @@ class StoryViewerPageFragment : } @SuppressLint("SetTextI18n") - private fun presentCaption(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View, storyPost: StoryPost) { + private fun presentCaption(caption: EmojiTextView, largeCaption: TextView, largeCaptionOverlay: View, storyPost: StoryPost) { val displayBody: CharSequence = if (storyPost.content is StoryPost.Content.AttachmentContent) { val displayBodySpan = SpannableString(storyPost.content.attachment.caption ?: "") val ranges: BodyRangeList? = storyPost.conversationMessage.messageRecord.messageRanges @@ -830,48 +831,24 @@ class StoryViewerPageFragment : "" } - storyNormalBottomGradient.visible = !displayBody.isNotEmpty() + storyNormalBottomGradient.visible = displayBody.isEmpty() storyCaptionBottomGradient.visible = displayBody.isNotEmpty() caption.text = displayBody largeCaption.text = displayBody caption.visible = displayBody.isNotEmpty() caption.requestLayout() + caption.movementMethod = LinkMovementMethod.getInstance() + caption.setOverflowText(getString(R.string.StoryViewerPageFragment__see_more)) + caption.maxLines = 5 + caption.text = displayBody - caption.doOnNextLayout { - val maxLines = 5 - if (displayBody.isNotEmpty() && caption.lineCount > maxLines) { - val lastCharShown = caption.layout.getLineVisibleEnd(maxLines - 1) - caption.maxLines = maxLines - - val seeMore = (getString(R.string.StoryViewerPageFragment__see_more)) - - val seeMoreWidth = caption.paint.measureText(seeMore) - var offset = seeMore.length - while (true) { - val start = lastCharShown - offset - if (start < 0) { - break - } - - val widthOfRemovedChunk = caption.paint.measureText(displayBody.subSequence(start, lastCharShown).toString()) - if (widthOfRemovedChunk > seeMoreWidth) { - break - } - - offset += 1 - } - - caption.text = displayBody.substring(0, lastCharShown - offset) + seeMore - } - - if (caption.text.length == displayBody.length) { - caption.setOnClickListener(null) - caption.isClickable = false - } else { - caption.setOnClickListener { - onShowCaptionOverlay(caption, largeCaption, largeCaptionOverlay) - } + if (caption.text.length == displayBody.length) { + caption.setOnClickListener(null) + caption.isClickable = false + } else { + caption.setOnClickListener { + onShowCaptionOverlay(caption, largeCaption, largeCaptionOverlay) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7cad656564..655af88739 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5218,7 +5218,7 @@ Copied to clipboard - … See More + See More Sending reply…