Show target domains for non-mention/non-hashtag links where the target domain is not provided or differs from the domain in the text (#2698)

* Show target domains for non-mention/non-hashtag links where the target domain is not provided or differs from the domain in the text.
Addresses #2694

* Add link signifier to the marked-up domain

* Back down on validating hashtags and mentions, don't markup _any_ urls where the text starts with #/@
This commit is contained in:
Levi Bard 2022-09-17 19:06:07 +02:00 committed by GitHub
parent c908ebb3f1
commit 687cffd540
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 123 additions and 1 deletions

View File

@ -57,7 +57,9 @@ fun getDomain(urlString: String?): String {
* @param listener to notify about particular spans that are clicked
*/
fun setClickableText(view: TextView, content: CharSequence, mentions: List<Mention>, tags: List<HashTag>?, listener: LinkListener) {
view.text = SpannableStringBuilder.valueOf(content).apply {
val spannableContent = markupHiddenUrls(view.context, content, mentions, tags, listener)
view.text = spannableContent.apply {
getSpans(0, content.length, URLSpan::class.java).forEach {
setClickableText(it, this, mentions, tags, listener)
}
@ -65,6 +67,29 @@ fun setClickableText(view: TextView, content: CharSequence, mentions: List<Menti
view.movementMethod = LinkMovementMethod.getInstance()
}
@VisibleForTesting
fun markupHiddenUrls(context: Context, content: CharSequence, mentions: List<Mention>, tags: List<HashTag>?, listener: LinkListener): SpannableStringBuilder {
val spannableContent = SpannableStringBuilder.valueOf(content)
val originalSpans = spannableContent.getSpans(0, content.length, URLSpan::class.java)
val obscuredLinkSpans = originalSpans.filter {
val text = spannableContent.subSequence(spannableContent.getSpanStart(it), spannableContent.getSpanEnd(it))
val firstCharacter = text[0]
firstCharacter != '#' &&
firstCharacter != '@' &&
getDomain(text.toString()) != getDomain(it.url)
}
for (span in obscuredLinkSpans) {
val start = spannableContent.getSpanStart(span)
val end = spannableContent.getSpanEnd(span)
val originalText = spannableContent.subSequence(start, end)
val replacementText = context.getString(R.string.url_domain_notifier, originalText, getDomain(span.url))
spannableContent.replace(start, end, replacementText) // this also updates the span locations
}
return spannableContent
}
@VisibleForTesting
fun setClickableText(
span: URLSpan,

View File

@ -658,6 +658,7 @@
<string name="tips_push_notification_migration">Re-login all accounts to enable push notification support.</string>
<string name="dialog_push_notification_migration">In order to use push notifications via UnifiedPush, Tusky needs permission to subscribe to notifications on your Mastodon server. This requires a re-login to change the OAuth scopes granted to Tusky. Using the re-login option here or in Account Preferences will preserve all of your local drafts and cache.</string>
<string name="dialog_push_notification_migration_other_accounts">You have re-logged into your current account to grant push subscription permission to Tusky. However, you still have other accounts that have not been migrated this way. Switch to them and re-login one by one in order to enable UnifiedPush notifications support.</string>
<string name="url_domain_notifier">%s (🔗 %s)</string>
</resources>

View File

@ -1,8 +1,11 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.text.SpannableStringBuilder
import android.text.style.URLSpan
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener
@ -29,6 +32,9 @@ class LinkHelperTest {
HashTag("mastodev", "https://example.com/Tags/mastodev"),
)
private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext
@Test
fun whenSettingClickableText_mentionUrlsArePreserved() {
val builder = SpannableStringBuilder()
@ -140,4 +146,94 @@ class LinkHelperTest {
Assert.assertEquals(domain, getDomain(url))
}
}
@Test
fun hiddenDomainsAreMarkedUp() {
val displayedContent = "This is a good place to go"
val maliciousDomain = "malicious.place"
val maliciousUrl = "https://$maliciousDomain/to/go"
val content = SpannableStringBuilder()
content.append(displayedContent, URLSpan(maliciousUrl), 0)
Assert.assertEquals(context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain), markupHiddenUrls(context, content, listOf(), listOf(), listener).toString())
}
@Test
fun fraudulentDomainsAreMarkedUp() {
val displayedContent = "https://tusky.app/"
val maliciousDomain = "malicious.place"
val maliciousUrl = "https://$maliciousDomain/to/go"
val content = SpannableStringBuilder()
content.append(displayedContent, URLSpan(maliciousUrl), 0)
Assert.assertEquals(context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain), markupHiddenUrls(context, content, listOf(), listOf(), listener).toString())
}
@Test fun multipleHiddenDomainsAreMarkedUp() {
val domains = listOf("one.place", "another.place", "athird.place")
val displayedContent = "link"
val content = SpannableStringBuilder()
for (domain in domains) {
content.append(displayedContent, URLSpan("https://$domain/foo/bar"), 0)
}
val markedUpContent = markupHiddenUrls(context, content, listOf(), listOf(), listener)
for (domain in domains) {
Assert.assertTrue(markedUpContent.contains(context.getString(R.string.url_domain_notifier, displayedContent, domain)))
}
}
@Test
fun validMentionsAreNotMarkedUp() {
val builder = SpannableStringBuilder()
for (mention in mentions) {
builder.append("@${mention.username}", URLSpan(mention.url), 0)
builder.append(" ")
}
val markedUpContent = markupHiddenUrls(context, builder, mentions, tags, listener)
for (mention in mentions) {
Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
}
}
@Test
fun invalidMentionsAreNotMarkedUp() {
val builder = SpannableStringBuilder()
for (mention in mentions) {
builder.append("@${mention.username}", URLSpan(mention.url), 0)
builder.append(" ")
}
val markedUpContent = markupHiddenUrls(context, builder, listOf(), tags, listener)
for (mention in mentions) {
Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
}
}
@Test
fun validTagsAreNotMarkedUp() {
val builder = SpannableStringBuilder()
for (tag in tags) {
builder.append("#${tag.name}", URLSpan(tag.url), 0)
builder.append(" ")
}
val markedUpContent = markupHiddenUrls(context, builder, mentions, tags, listener)
for (tag in tags) {
Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
}
}
@Test
fun invalidTagsAreNotMarkedUp() {
val builder = SpannableStringBuilder()
for (tag in tags) {
builder.append("#${tag.name}", URLSpan(tag.url), 0)
builder.append(" ")
}
val markedUpContent = markupHiddenUrls(context, builder, mentions, listOf(), listener)
for (tag in tags) {
Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
}
}
}