From 8d8519b52e50dc52aee03c69ec707d645ed7c89f Mon Sep 17 00:00:00 2001 From: Sagar <85388413+Sagar0-0@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:11:29 +0530 Subject: [PATCH] Linkify story captions. --- .../viewer/page/StoryViewerPageFragment.kt | 37 ++++++++++++++++--- .../layout/stories_viewer_fragment_page.xml | 17 +++------ 2 files changed, 38 insertions(+), 16 deletions(-) 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 a770775396..45f43bc6fb 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 @@ -9,11 +9,14 @@ import android.graphics.Rect import android.graphics.Typeface import android.media.AudioManager import android.os.Bundle +import android.text.Spannable import android.text.SpannableString import android.text.Spanned -import android.text.method.ScrollingMovementMethod +import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.text.style.StyleSpan +import android.text.style.URLSpan +import android.text.util.Linkify import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector @@ -27,6 +30,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.os.bundleOf +import androidx.core.text.util.LinkifyCompat import androidx.core.view.GestureDetectorCompat import androidx.core.view.animation.PathInterpolatorCompat import androidx.fragment.app.DialogFragment @@ -89,6 +93,8 @@ import org.thoughtcrime.securesms.util.AvatarUtil import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.Debouncer +import org.thoughtcrime.securesms.util.LinkUtil +import org.thoughtcrime.securesms.util.LongClickCopySpan import org.thoughtcrime.securesms.util.LongClickMovementMethod import org.thoughtcrime.securesms.util.Projection import org.thoughtcrime.securesms.util.ServiceUtil @@ -941,7 +947,7 @@ class StoryViewerPageFragment : if (ranges != null && displayBodySpan.isNotEmpty()) { MessageStyler.style(storyPost.conversationMessage.messageRecord.dateSent, ranges, displayBodySpan) } - + linkifyUrlLinks(displayBodySpan) displayBodySpan } else { "" @@ -960,9 +966,9 @@ class StoryViewerPageFragment : caption.setOverflowText(overflow) caption.maxLines = 5 caption.text = displayBody - caption.setMaxLength(280) + caption.setMaxLength(SMALL_CAPTION_TEXT_MAX_LENGTH) - if (caption.text.length == displayBody.length) { + if (displayBody.length <= SMALL_CAPTION_TEXT_MAX_LENGTH) { caption.setOnClickListener(null) caption.isClickable = false } else { @@ -972,13 +978,32 @@ class StoryViewerPageFragment : } } + fun linkifyUrlLinks(spannable: Spannable) { + val hasLinks = LinkifyCompat.addLinks(spannable, CAPTION_LINK_PATTERN) + + if (hasLinks) { + spannable.getSpans(0, spannable.length, URLSpan::class.java) + .filterNot { url -> LinkUtil.isLegalUrl(url.url) } + .forEach { spannable.removeSpan(it) } + + val urlSpans = spannable.getSpans(0, spannable.length, URLSpan::class.java) + + for (urlSpan in urlSpans) { + val start = spannable.getSpanStart(urlSpan) + val end = spannable.getSpanEnd(urlSpan) + val span = LongClickCopySpan(urlSpan.url) + spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } + private fun onShowCaptionOverlay(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View) { sharedViewModel.setIsChildScrolling(true) caption.visible = false largeCaption.visible = true largeCaptionOverlay.visible = true - largeCaption.movementMethod = ScrollingMovementMethod() + largeCaption.movementMethod = LinkMovementMethod.getInstance() largeCaption.scrollY = 0 largeCaption.setOnClickListener { onHideCaptionOverlay(caption, largeCaption, largeCaptionOverlay) @@ -1273,6 +1298,8 @@ class StoryViewerPageFragment : private val CHARACTERS_PER_SECOND = 15L private val DEFAULT_DURATION = TimeUnit.SECONDS.toMillis(5) private val ONBOARDING_DURATION = TimeUnit.SECONDS.toMillis(10) + private const val SMALL_CAPTION_TEXT_MAX_LENGTH = 280 + private const val CAPTION_LINK_PATTERN = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS private const val ARGS = "args" diff --git a/app/src/main/res/layout/stories_viewer_fragment_page.xml b/app/src/main/res/layout/stories_viewer_fragment_page.xml index 0ab97505a4..df867b3ae7 100644 --- a/app/src/main/res/layout/stories_viewer_fragment_page.xml +++ b/app/src/main/res/layout/stories_viewer_fragment_page.xml @@ -138,24 +138,19 @@ + app:layout_constraintStart_toStartOf="parent">