Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

correctly count emojis when composing a post #4152

Merged
merged 4 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.icu.text.BreakIterator
import android.net.Uri
import android.os.Build
import android.os.Bundle
Expand Down Expand Up @@ -1406,7 +1407,7 @@ class ComposeActivity :
*/
@JvmStatic
fun statusLength(body: Spanned, contentWarning: Spanned?, urlLength: Int): Int {
var length = body.length - body.getSpans(0, body.length, URLSpan::class.java)
var length = body.toString().perceivedCharacterLength() - body.getSpans(0, body.length, URLSpan::class.java)
.fold(0) { acc, span ->
// Accumulate a count of characters to be *ignored* in the final length
acc + when (span) {
Expand All @@ -1419,15 +1420,25 @@ class ComposeActivity :
}
else -> {
// Expected to be negative if the URL length < maxUrlLength
span.url.length - urlLength
span.url.perceivedCharacterLength() - urlLength
}
}
}

// Content warning text is treated as is, URLs or mentions there are not special
contentWarning?.let { length += it.length }

contentWarning?.let { length += it.toString().perceivedCharacterLength() }
return length
}

// String.length would count emojis as multiple characters but Mastodon counts them as 1, so we need this workaround
private fun String.perceivedCharacterLength(): Int {
val breakIterator = BreakIterator.getCharacterInstance()
breakIterator.setText(this)
var count = 0
while (breakIterator.next() != BreakIterator.DONE) {
count++
}
return count
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* see <http://www.gnu.org/licenses>.
*/

package com.keylesspalace.tusky.components.compose.ComposeActivity
package com.keylesspalace.tusky.components.compose

import android.content.Intent
import android.os.Looper.getMainLooper
Expand All @@ -24,8 +24,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import at.connyduck.calladapter.networkresult.NetworkResult
import com.google.gson.Gson
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
Expand Down Expand Up @@ -252,15 +250,29 @@ class ComposeActivityTest {
fun whenTextContainsNoUrl_everyCharacterIsCounted() {
val content = "This is test content please ignore thx "
insertSomeTextInContent(content)
assertEquals(activity.calculateTextLength(), content.length)
assertEquals(content.length, activity.calculateTextLength())
}

@Test
fun whenTextContainsEmoji_emojisAreCountedAsOneCharacter() {
val content = "Test 😜"
insertSomeTextInContent(content)
assertEquals(6, activity.calculateTextLength())
}

@Test
fun whenTextContainsUrlWithEmoji_ellipsizedUrlIsCountedCorrectly() {
val content = "https://🤪.com"
insertSomeTextInContent(content)
assertEquals(InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL, activity.calculateTextLength())
}

@Test
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted() {
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)
assertEquals(activity.calculateTextLength(), additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL)
assertEquals(additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL, activity.calculateTextLength())
}

@Test
Expand All @@ -269,15 +281,15 @@ class ComposeActivityTest {
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(shortUrl + additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2))
assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength())
}

@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)
assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2))
assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength())
}

@Test
Expand All @@ -289,7 +301,7 @@ class ComposeActivityTest {
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength)
assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength())
}

@Test
Expand All @@ -301,7 +313,7 @@ class ComposeActivityTest {
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength)
assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength())
}

@Test
Expand All @@ -314,7 +326,7 @@ class ComposeActivityTest {
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(shortUrl + additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
}

@Test
Expand All @@ -327,7 +339,7 @@ class ComposeActivityTest {
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(shortUrl + additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
}

@Test
Expand All @@ -339,7 +351,7 @@ class ComposeActivityTest {
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(url + additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
}

@Test
Expand All @@ -351,7 +363,7 @@ class ComposeActivityTest {
setupActivity()
shadowOf(getMainLooper()).idle()
insertSomeTextInContent(url + additionalContent + url)
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */

package com.keylesspalace.tusky.components.compose.ComposeTokenizer
package com.keylesspalace.tusky.components.compose

import com.keylesspalace.tusky.components.compose.ComposeTokenizer
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,29 @@
* see <http://www.gnu.org/licenses>.
*/

package com.keylesspalace.tusky.components.compose.ComposeActivity
package com.keylesspalace.tusky.components.compose

import com.keylesspalace.tusky.SpanUtilsTest
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.util.highlightSpans
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.robolectric.ParameterizedRobolectricTestRunner

@RunWith(Parameterized::class)
@RunWith(ParameterizedRobolectricTestRunner::class)
class StatusLengthTest(
private val text: String,
private val expectedLength: Int
) {
companion object {
@Parameterized.Parameters(name = "{0}")
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
@JvmStatic
fun data(): Iterable<Any> {
return listOf(
arrayOf("", 0),
arrayOf(" ", 1),
arrayOf("123", 3),
arrayOf("🫣", 1),
// "@user@server" should be treated as "@user"
arrayOf("123 @[email protected]", 12),
// URLs under 23 chars are treated as 23 chars
Expand Down