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
|
* @param listener to notify about particular spans that are clicked
|
||||||
*/
|
*/
|
||||||
fun setClickableText(view: TextView, content: CharSequence, mentions: List<Mention>, tags: List<HashTag>?, listener: LinkListener) {
|
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 {
|
getSpans(0, content.length, URLSpan::class.java).forEach {
|
||||||
setClickableText(it, this, mentions, tags, listener)
|
setClickableText(it, this, mentions, tags, listener)
|
||||||
}
|
}
|
||||||
@ -65,6 +67,29 @@ fun setClickableText(view: TextView, content: CharSequence, mentions: List<Menti
|
|||||||
view.movementMethod = LinkMovementMethod.getInstance()
|
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
|
@VisibleForTesting
|
||||||
fun setClickableText(
|
fun setClickableText(
|
||||||
span: URLSpan,
|
span: URLSpan,
|
||||||
|
@ -658,6 +658,7 @@
|
|||||||
<string name="tips_push_notification_migration">Re-login all accounts to enable push notification support.</string>
|
<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">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="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>
|
</resources>
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package com.keylesspalace.tusky.util
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
@ -29,6 +32,9 @@ class LinkHelperTest {
|
|||||||
HashTag("mastodev", "https://example.com/Tags/mastodev"),
|
HashTag("mastodev", "https://example.com/Tags/mastodev"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val context: Context
|
||||||
|
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun whenSettingClickableText_mentionUrlsArePreserved() {
|
fun whenSettingClickableText_mentionUrlsArePreserved() {
|
||||||
val builder = SpannableStringBuilder()
|
val builder = SpannableStringBuilder()
|
||||||
@ -140,4 +146,94 @@ class LinkHelperTest {
|
|||||||
Assert.assertEquals(domain, getDomain(url))
|
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