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:
parent
c908ebb3f1
commit
687cffd540
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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)})"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user