From 7a391f590fe4207d4638c133f9ef4ba7c0be6357 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 8 Dec 2023 11:06:01 +0100 Subject: [PATCH 1/4] correctly count emojis when composing a post --- .../components/compose/ComposeActivity.kt | 18 +++++++--- .../ComposeActivity/ComposeActivityTest.kt | 34 +++++++++++++------ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index a87b60f587..4413279c8a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -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 @@ -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().perceivedCharterLength() - 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) { @@ -1419,15 +1420,24 @@ class ComposeActivity : } else -> { // Expected to be negative if the URL length < maxUrlLength - span.url.length - urlLength + span.url.perceivedCharterLength() - 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().perceivedCharterLength() } return length } + + private fun String.perceivedCharterLength(): Int { + val breakIterator = BreakIterator.getCharacterInstance() + breakIterator.setText(this) + var count = 0 + while (breakIterator.next() != BreakIterator.DONE) { + count++ + } + return count + } } } diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/ComposeActivityTest.kt index 451b5c09a0..bc7dbf6e0b 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/ComposeActivityTest.kt @@ -252,7 +252,21 @@ 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 @@ -260,7 +274,7 @@ 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(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 @@ -269,7 +283,7 @@ 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 @@ -277,7 +291,7 @@ 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(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 @@ -289,7 +303,7 @@ class ComposeActivityTest { setupActivity() shadowOf(getMainLooper()).idle() insertSomeTextInContent(additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength) + assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength()) } @Test @@ -301,7 +315,7 @@ class ComposeActivityTest { setupActivity() shadowOf(getMainLooper()).idle() insertSomeTextInContent(additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength) + assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength()) } @Test @@ -314,7 +328,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 @@ -327,7 +341,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 @@ -339,7 +353,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 @@ -351,7 +365,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 From b33d84cfaa51d59a8907c524df271d63408a70d7 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 8 Dec 2023 11:10:56 +0100 Subject: [PATCH 2/4] add comment why String.perceivedCharacterLength is necessary --- .../keylesspalace/tusky/components/compose/ComposeActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 4413279c8a..4c01971065 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -1430,6 +1430,7 @@ class ComposeActivity : return length } + // String.length would count emojis as multiple characters but Mastodon counts them as 1, so we need this workaround private fun String.perceivedCharterLength(): Int { val breakIterator = BreakIterator.getCharacterInstance() breakIterator.setText(this) From f47dad6c17844f3dc74a1d307782d6d609dd1ccb Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 8 Dec 2023 11:27:24 +0100 Subject: [PATCH 3/4] fix tests --- .../{ComposeActivity => }/ComposeActivityTest.kt | 4 +--- .../{ComposeTokenizer => }/ComposeTokenizerTest.kt | 3 +-- .../compose/{ComposeActivity => }/StatusLengthTest.kt | 10 +++++----- 3 files changed, 7 insertions(+), 10 deletions(-) rename app/src/test/java/com/keylesspalace/tusky/components/compose/{ComposeActivity => }/ComposeActivityTest.kt (99%) rename app/src/test/java/com/keylesspalace/tusky/components/compose/{ComposeTokenizer => }/ComposeTokenizerTest.kt (96%) rename app/src/test/java/com/keylesspalace/tusky/components/compose/{ComposeActivity => }/StatusLengthTest.kt (90%) diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt similarity index 99% rename from app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/ComposeActivityTest.kt rename to app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt index bc7dbf6e0b..f46550e047 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt @@ -15,7 +15,7 @@ * see . */ -package com.keylesspalace.tusky.components.compose.ComposeActivity +package com.keylesspalace.tusky.components.compose import android.content.Intent import android.os.Looper.getMainLooper @@ -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 diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer/ComposeTokenizerTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizerTest.kt similarity index 96% rename from app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer/ComposeTokenizerTest.kt rename to app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizerTest.kt index 04c692bce0..3c801dcbb2 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer/ComposeTokenizerTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizerTest.kt @@ -13,9 +13,8 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -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 diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/StatusLengthTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/StatusLengthTest.kt similarity index 90% rename from app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/StatusLengthTest.kt rename to app/src/test/java/com/keylesspalace/tusky/components/compose/StatusLengthTest.kt index 81c40a556f..5f80963052 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivity/StatusLengthTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/StatusLengthTest.kt @@ -15,29 +15,29 @@ * see . */ -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 { return listOf( arrayOf("", 0), arrayOf(" ", 1), arrayOf("123", 3), + arrayOf("🫣", 1), // "@user@server" should be treated as "@user" arrayOf("123 @example@example.org", 12), // URLs under 23 chars are treated as 23 chars From 8d0a125cf69024b67214757d0fa6e2eec9696c1b Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 8 Dec 2023 11:27:34 +0100 Subject: [PATCH 4/4] fix typo --- .../tusky/components/compose/ComposeActivity.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 4c01971065..0ee91e48f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -1407,7 +1407,7 @@ class ComposeActivity : */ @JvmStatic fun statusLength(body: Spanned, contentWarning: Spanned?, urlLength: Int): Int { - var length = body.toString().perceivedCharterLength() - 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) { @@ -1420,18 +1420,18 @@ class ComposeActivity : } else -> { // Expected to be negative if the URL length < maxUrlLength - span.url.perceivedCharterLength() - urlLength + span.url.perceivedCharacterLength() - urlLength } } } // Content warning text is treated as is, URLs or mentions there are not special - contentWarning?.let { length += it.toString().perceivedCharterLength() } + 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.perceivedCharterLength(): Int { + private fun String.perceivedCharacterLength(): Int { val breakIterator = BreakIterator.getCharacterInstance() breakIterator.setText(this) var count = 0