mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-10 15:58:36 +01:00
+8
-10
@@ -14,6 +14,7 @@ import androidx.core.text.util.LinkifyCompat
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan
|
||||
import org.thoughtcrime.securesms.util.LinkUtil
|
||||
import org.thoughtcrime.securesms.util.Linkification
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler
|
||||
import org.thoughtcrime.securesms.util.hasOnlyThumbnail
|
||||
|
||||
@@ -28,24 +29,21 @@ object V2ConversationItemUtils {
|
||||
|
||||
@JvmStatic
|
||||
fun linkifyUrlLinks(messageBody: Spannable, shouldLinkifyAllLinks: Boolean, urlClickHandler: UrlClickHandler) {
|
||||
val linkPattern = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS
|
||||
val hasLinks = LinkifyCompat.addLinks(messageBody, if (shouldLinkifyAllLinks) linkPattern else 0)
|
||||
|
||||
if (!hasLinks) {
|
||||
if (!shouldLinkifyAllLinks) {
|
||||
return
|
||||
}
|
||||
|
||||
messageBody.getSpans(0, messageBody.length, URLSpan::class.java)
|
||||
.filterNot { LinkUtil.isLegalUrl(it.url) }
|
||||
.forEach(messageBody::removeSpan)
|
||||
LinkifyCompat.addLinks(messageBody, Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
|
||||
Linkification.applyWebUrlSpans(messageBody)
|
||||
|
||||
messageBody.getSpans(0, messageBody.length, URLSpan::class.java).forEach { urlSpan ->
|
||||
val url = urlSpan.url
|
||||
val start = messageBody.getSpanStart(urlSpan)
|
||||
val end = messageBody.getSpanEnd(urlSpan)
|
||||
val span = InterceptableLongClickCopyLinkSpan(urlSpan.url, urlClickHandler)
|
||||
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
messageBody.removeSpan(urlSpan)
|
||||
if (LinkUtil.isLegalUrl(url)) {
|
||||
messageBody.setSpan(InterceptableLongClickCopyLinkSpan(url, urlClickHandler), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.util.LinkUtil;
|
||||
import org.thoughtcrime.securesms.util.Linkification;
|
||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
||||
|
||||
public final class GroupDescriptionUtil {
|
||||
@@ -38,21 +37,16 @@ public final class GroupDescriptionUtil {
|
||||
SpannableString descriptionSpannable = new SpannableString(scrubbedDescription);
|
||||
|
||||
if (linkify) {
|
||||
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
||||
boolean hasLinks = LinkifyCompat.addLinks(descriptionSpannable, linkPattern);
|
||||
LinkifyCompat.addLinks(descriptionSpannable, Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS);
|
||||
Linkification.applyWebUrlSpans(descriptionSpannable);
|
||||
|
||||
if (hasLinks) {
|
||||
Stream.of(descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class))
|
||||
.filter(url -> !LinkUtil.isLegalUrl(url.getURL()))
|
||||
.forEach(descriptionSpannable::removeSpan);
|
||||
|
||||
URLSpan[] urlSpans = descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class);
|
||||
|
||||
for (URLSpan urlSpan : urlSpans) {
|
||||
int start = descriptionSpannable.getSpanStart(urlSpan);
|
||||
int end = descriptionSpannable.getSpanEnd(urlSpan);
|
||||
URLSpan span = new LongClickCopySpan(urlSpan.getURL());
|
||||
descriptionSpannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
for (URLSpan urlSpan : descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class)) {
|
||||
String url = urlSpan.getURL();
|
||||
int start = descriptionSpannable.getSpanStart(urlSpan);
|
||||
int end = descriptionSpannable.getSpanEnd(urlSpan);
|
||||
descriptionSpannable.removeSpan(urlSpan);
|
||||
if (LinkUtil.isLegalUrl(url)) {
|
||||
descriptionSpannable.setSpan(new LongClickCopySpan(url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.signal.core.util.Linkifier;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.LinkUtil;
|
||||
import org.thoughtcrime.securesms.util.Linkification;
|
||||
import org.signal.core.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
@@ -53,16 +51,15 @@ public final class LinkPreviewUtil {
|
||||
* @return All URLs allowed as previews in the source text.
|
||||
*/
|
||||
public static @NonNull Links findValidPreviewUrls(@NonNull String text) {
|
||||
SpannableString spannable = new SpannableString(text);
|
||||
boolean found = LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS);
|
||||
|
||||
if (!found) {
|
||||
List<Linkifier.DetectedLink> detected = Linkification.findWebLinks(text);
|
||||
if (detected.isEmpty()) {
|
||||
return Links.EMPTY;
|
||||
}
|
||||
|
||||
return new Links(Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
|
||||
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
|
||||
.filter(link -> LinkUtil.isValidPreviewUrl(link.url)).collect(Collectors.toList()));
|
||||
return new Links(detected.stream()
|
||||
.map(d -> new Link(d.getUrl(), d.getStart()))
|
||||
.filter(link -> LinkUtil.isValidPreviewUrl(link.url))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
|
||||
|
||||
@@ -7,8 +7,6 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -23,11 +21,11 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.Linkifier;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.debuglogsviewer.DebugLogsViewer;
|
||||
import org.thoughtcrime.securesms.BaseActivity;
|
||||
@@ -36,6 +34,7 @@ import org.thoughtcrime.securesms.components.ConversationSearchBottomBar;
|
||||
import org.thoughtcrime.securesms.components.ProgressCard;
|
||||
import org.thoughtcrime.securesms.components.SearchView;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Linkification;
|
||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.signal.core.ui.util.ThemeUtil;
|
||||
@@ -449,15 +448,8 @@ public class SubmitDebugLogActivity extends BaseActivity {
|
||||
TextView dialogView = new TextView(builder.getContext());
|
||||
LongClickCopySpan longClickUrl = new LongClickCopySpan(url);
|
||||
|
||||
|
||||
LinkifyCompat.addLinks(spannableDialogText, Linkify.WEB_URLS);
|
||||
|
||||
URLSpan[] spans = spannableDialogText.getSpans(0, spannableDialogText.length(), URLSpan.class);
|
||||
for (URLSpan span : spans) {
|
||||
int start = spannableDialogText.getSpanStart(span);
|
||||
int end = spannableDialogText.getSpanEnd(span);
|
||||
|
||||
spannableDialogText.setSpan(longClickUrl, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
for (Linkifier.DetectedLink link : Linkification.findWebLinks(dialogText)) {
|
||||
spannableDialogText.setSpan(longClickUrl, link.getStart(), link.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
dialogView.setText(spannableDialogText);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.nicknames
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.util.Linkify
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -40,6 +41,7 @@ import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Linkification
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@@ -142,11 +144,7 @@ private fun ViewNoteBottomSheetContent(
|
||||
)
|
||||
)
|
||||
|
||||
val mask = if (LocalInspectionMode.current) {
|
||||
Linkify.WEB_URLS
|
||||
} else {
|
||||
Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS
|
||||
}
|
||||
val isInspection = LocalInspectionMode.current
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
@@ -161,9 +159,12 @@ private fun ViewNoteBottomSheetContent(
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 48.dp)
|
||||
) {
|
||||
it.text = note
|
||||
|
||||
LinkifyCompat.addLinks(it, mask)
|
||||
val spannable = SpannableString(note)
|
||||
if (!isInspection) {
|
||||
LinkifyCompat.addLinks(spannable, Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
|
||||
}
|
||||
Linkification.applyWebUrlSpans(spannable)
|
||||
it.text = spannable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-14
@@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.util.AvatarUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.LinkUtil
|
||||
import org.thoughtcrime.securesms.util.Linkification
|
||||
import org.thoughtcrime.securesms.util.LongClickCopySpan
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
@@ -993,20 +994,16 @@ class StoryViewerPageFragment :
|
||||
}
|
||||
|
||||
fun linkifyUrlLinks(spannable: Spannable) {
|
||||
val hasLinks = LinkifyCompat.addLinks(spannable, CAPTION_LINK_PATTERN)
|
||||
LinkifyCompat.addLinks(spannable, Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
|
||||
Linkification.applyWebUrlSpans(spannable)
|
||||
|
||||
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)
|
||||
spannable.getSpans(0, spannable.length, URLSpan::class.java).forEach { urlSpan ->
|
||||
val url = urlSpan.url
|
||||
val start = spannable.getSpanStart(urlSpan)
|
||||
val end = spannable.getSpanEnd(urlSpan)
|
||||
spannable.removeSpan(urlSpan)
|
||||
if (LinkUtil.isLegalUrl(url)) {
|
||||
spannable.setSpan(LongClickCopySpan(url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1315,7 +1312,6 @@ class StoryViewerPageFragment :
|
||||
private val ONBOARDING_DURATION = TimeUnit.SECONDS.toMillis(10)
|
||||
private const val SMALL_CAPTION_TEXT_MAX_LENGTH = 280
|
||||
private const val SMALL_CAPTION_TEXT_MAX_LINES = 5
|
||||
private const val CAPTION_LINK_PATTERN = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS
|
||||
|
||||
private const val ARGS = "args"
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import org.signal.core.util.Linkifier
|
||||
import org.signal.core.util.Linkifier.DetectedLink
|
||||
import org.signal.core.util.addDetectedLinks
|
||||
|
||||
/**
|
||||
* Temporary abstraction while we switch over to the new [Linkifier] via remote config.
|
||||
* When the remote config is off, we fallback to the pre-existing android link logic.
|
||||
*/
|
||||
object Linkification {
|
||||
|
||||
/**
|
||||
* Adds [URLSpan]s for web URLs in [spannable]. Returns `true` if at least one span was added.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun applyWebUrlSpans(spannable: Spannable): Boolean {
|
||||
return if (RemoteConfig.useNewLinkifier) {
|
||||
spannable.addDetectedLinks()
|
||||
} else {
|
||||
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds web URLs in [text].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun findWebLinks(text: CharSequence): List<DetectedLink> {
|
||||
if (RemoteConfig.useNewLinkifier) {
|
||||
return Linkifier.findLinks(text)
|
||||
}
|
||||
|
||||
val spannable = SpannableString(text)
|
||||
if (!LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return spannable.getSpans(0, spannable.length, URLSpan::class.java).map { span ->
|
||||
DetectedLink(
|
||||
start = spannable.getSpanStart(span),
|
||||
end = spannable.getSpanEnd(span),
|
||||
url = span.url
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
/**
|
||||
* LinkifyCompat.addLinks() will strip pre-existing URLSpans. This acts as a way to
|
||||
* indicate where a link should be added without being stripped. The consumer is
|
||||
* responsible for replacing the placeholder with an actual URLSpan.
|
||||
* Marks a region where a link should later be applied, without storing it as an actual [URLSpan].
|
||||
* The consumer is responsible for replacing the placeholder with an appropriate URLSpan when the
|
||||
* text is rendered.
|
||||
*/
|
||||
class PlaceholderURLSpan(url: String) : android.text.Annotation("placeholderUrl", url)
|
||||
|
||||
@@ -1365,5 +1365,16 @@ object RemoteConfig {
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether to use our custom [org.signal.core.util.Linkifier] for web URL detection.
|
||||
*/
|
||||
@JvmStatic
|
||||
@get:JvmName("useNewLinkifier")
|
||||
val useNewLinkifier: Boolean by remoteBoolean(
|
||||
key = "android.useNewLinkifier",
|
||||
defaultValue = false,
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user