Behave like Mastodon web ui and only count URLs as 23 characters when composing (#629)
* Refactor-all-the-things version of the fix for issue #573 * Migrate SpanUtils to kotlin because why not * Minimal fix for issue #573 * Add tests for compose spanning * Clean up code suggestions * Make FakeSpannable.getSpans implementation less awkward * Add secondary validation pass for urls * Address code review feedback * Fixup type filtering in FakeSpannable again * Make all mentions in compose activity use the default link color
This commit is contained in:
parent
41743b0dca
commit
7e1f5edeca
5 changed files with 326 additions and 154 deletions
|
@ -26,6 +26,7 @@ import com.keylesspalace.tusky.entity.Instance
|
|||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import okhttp3.Request
|
||||
import okhttp3.ResponseBody
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
@ -198,6 +199,23 @@ class ComposeActivityTest {
|
|||
assertEquals(ComposeActivity.STATUS_CHARACTER_LIMIT, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsUrl_onlyEllipsizedURLIsCountedAgainstCharacterLimit() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = "Check out this @image #search result: "
|
||||
insertSomeTextInContent(additionalContent + url)
|
||||
Assert.assertEquals(activity.calculateRemainingCharacters(), activity.maximumTootCharacters - additionalContent.length - ComposeActivity.MAXIMUM_URL_LENGTH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsMultipleURLs_allURLsGetEllipsized() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
insertSomeTextInContent(url + additionalContent + url)
|
||||
Assert.assertEquals(activity.calculateRemainingCharacters(),
|
||||
activity.maximumTootCharacters - additionalContent.length - (ComposeActivity.MAXIMUM_URL_LENGTH * 2))
|
||||
}
|
||||
|
||||
private fun clickUp() {
|
||||
val menuItem = RoboMenuItem(android.R.id.home)
|
||||
activity.onOptionsItemSelected(menuItem)
|
||||
|
@ -207,8 +225,8 @@ class ComposeActivityTest {
|
|||
activity.onBackPressed()
|
||||
}
|
||||
|
||||
private fun insertSomeTextInContent() {
|
||||
activity.findViewById<EditText>(R.id.composeEditField).setText("Some text")
|
||||
private fun insertSomeTextInContent(text: String? = null) {
|
||||
activity.findViewById<EditText>(R.id.composeEditField).setText(text ?: "Some text")
|
||||
}
|
||||
|
||||
private fun getInstanceWithMaximumTootCharacters(maximumTootCharacters: Int?): Instance
|
||||
|
|
151
app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt
Normal file
151
app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt
Normal file
|
@ -0,0 +1,151 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.text.Spannable
|
||||
import com.keylesspalace.tusky.util.highlightSpans
|
||||
import junit.framework.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
class SpanUtilsTest {
|
||||
@Test
|
||||
fun matchesMixedSpans() {
|
||||
val input = "one #one two @two three https://thr.ee/meh?foo=bar&wat=@at#hmm four #four five @five"
|
||||
val inputSpannable = FakeSpannable(input)
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(5, spans.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesntMergeAdjacentURLs() {
|
||||
val firstURL = "http://first.thing"
|
||||
val secondURL = "https://second.thing"
|
||||
val inputSpannable = FakeSpannable("${firstURL} ${secondURL}")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(2, spans.size)
|
||||
Assert.assertEquals(firstURL.length, spans[0].end - spans[0].start)
|
||||
Assert.assertEquals(secondURL.length, spans[1].end - spans[1].start)
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class MatchingTests(private val thingToHighlight: String) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
return listOf(
|
||||
"@mention",
|
||||
"#tag",
|
||||
"https://thr.ee/meh?foo=bar&wat=@at#hmm",
|
||||
"http://thr.ee/meh?foo=bar&wat=@at#hmm"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchesSpanAtStart() {
|
||||
val inputSpannable = FakeSpannable(thingToHighlight)
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(1, spans.size)
|
||||
Assert.assertEquals(thingToHighlight.length, spans[0].end - spans[0].start)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchesSpanNotAtStart() {
|
||||
val inputSpannable = FakeSpannable(" ${thingToHighlight}")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(1, spans.size)
|
||||
Assert.assertEquals(thingToHighlight.length, spans[0].end - spans[0].start)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotMatchSpanEmbeddedInText() {
|
||||
val inputSpannable = FakeSpannable("aa${thingToHighlight}aa")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertTrue(spans.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotMatchSpanEmbeddedInAnotherSpan() {
|
||||
val inputSpannable = FakeSpannable("@aa${thingToHighlight}aa")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(1, spans.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spansDoNotOverlap() {
|
||||
val begin = "@begin"
|
||||
val end = "#end"
|
||||
val inputSpannable = FakeSpannable("${begin} ${thingToHighlight} ${end}")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(3, spans.size)
|
||||
|
||||
val middleSpan = spans.single ({ span -> span.start > 0 && span.end < inputSpannable.lastIndex })
|
||||
Assert.assertEquals(begin.length + 1, middleSpan.start)
|
||||
Assert.assertEquals(inputSpannable.length - end.length - 1, middleSpan.end)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeSpannable(private val text: String) : Spannable {
|
||||
val spans = mutableListOf<BoundedSpan>()
|
||||
|
||||
override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) {
|
||||
spans.add(BoundedSpan(what, start, end))
|
||||
}
|
||||
|
||||
override fun <T : Any?> getSpans(start: Int, end: Int, type: Class<T>?): Array<T> {
|
||||
val matching = if (type == null) {
|
||||
ArrayList<T>()
|
||||
} else {
|
||||
spans.filter ({ it.start >= start && it.end <= end && type?.isAssignableFrom(it.span?.javaClass) })
|
||||
.map({ it -> it.span })
|
||||
.let { ArrayList(it) }
|
||||
}
|
||||
return matching.toArray() as Array<T>
|
||||
}
|
||||
|
||||
override fun removeSpan(what: Any?) {
|
||||
spans.removeIf({ span -> span.span == what})
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return text
|
||||
}
|
||||
|
||||
override val length: Int
|
||||
get() = text.length
|
||||
|
||||
class BoundedSpan(val span: Any?, val start: Int, val end: Int)
|
||||
|
||||
override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanEnd(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanFlags(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun get(index: Int): Char {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanStart(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue