mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge 94872734a3c800fdff66849ec41d361062920600 into c127eb83ab94c069c32d37530d2faecd381cd2a8
This commit is contained in:
commit
9fe5cc3764
@ -1,9 +1,19 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import java.text.BreakIterator
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI constants/utilities for nickname rendering.
|
* UI constants/utilities for nickname rendering.
|
||||||
*/
|
*/
|
||||||
fun truncateNickname(name: String, maxLen: Int = com.bitchat.android.util.AppConstants.UI.MAX_NICKNAME_LENGTH): String {
|
fun truncateNickname(name: String, maxLen: Int = com.bitchat.android.util.AppConstants.UI.MAX_NICKNAME_LENGTH): String {
|
||||||
return if (name.length <= maxLen) name else name.take(maxLen)
|
if (name.length <= maxLen) return name
|
||||||
}
|
|
||||||
|
|
||||||
|
// The limit counts UTF-16 code units, so cutting at it can land inside a
|
||||||
|
// surrogate pair and leave a lone surrogate that renders as a tofu box, or
|
||||||
|
// split a ZWJ sequence into its parts. Step back to the last grapheme
|
||||||
|
// boundary at or before the limit so what is shown is always whole.
|
||||||
|
val boundaries = BreakIterator.getCharacterInstance()
|
||||||
|
boundaries.setText(name)
|
||||||
|
val end = boundaries.preceding(maxLen + 1)
|
||||||
|
return if (end == BreakIterator.DONE) "" else name.substring(0, end)
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ChatUIConstantsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a name inside the limit is returned unchanged`() {
|
||||||
|
assertEquals("alice", truncateNickname("alice", maxLen = 15))
|
||||||
|
assertEquals("exactlyfifteen!", truncateNickname("exactlyfifteen!", maxLen = 15))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an ascii name is still cut at the limit`() {
|
||||||
|
assertEquals("hello world thi", truncateNickname("hello world this is long", maxLen = 15))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The limit counts UTF-16 code units, so the 15th unit of this name is the
|
||||||
|
* high surrogate of the emoji. Cutting there leaves a lone surrogate that
|
||||||
|
* renders as a tofu box.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `a surrogate pair is never split`() {
|
||||||
|
val truncated = truncateNickname("abcdefghijklmn😀", maxLen = 15)
|
||||||
|
|
||||||
|
assertEquals("abcdefghijklmn", truncated)
|
||||||
|
assertTrue(
|
||||||
|
"truncated name must not end in a lone surrogate",
|
||||||
|
truncated.none { Character.isSurrogate(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an emoji that fits is kept whole`() {
|
||||||
|
assertEquals("abcdefghijkl😀m", truncateNickname("abcdefghijkl😀mno", maxLen = 15))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A ZWJ sequence is one grapheme, so it is kept or dropped as a whole. */
|
||||||
|
@Test
|
||||||
|
fun `a zwj sequence is not split into its parts`() {
|
||||||
|
val family = "👨👩👧"
|
||||||
|
val truncated = truncateNickname(family + " family chat", maxLen = 15)
|
||||||
|
|
||||||
|
assertEquals(family + " family", truncated)
|
||||||
|
assertTrue("the family must survive whole", truncated.startsWith(family))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a name made only of one long grapheme truncates to empty rather than half a character`() {
|
||||||
|
val family = "👨👩👧👦"
|
||||||
|
val truncated = truncateNickname(family, maxLen = 5)
|
||||||
|
|
||||||
|
assertEquals("", truncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every truncation stays within the limit and holds no partial character`() {
|
||||||
|
val samples = listOf(
|
||||||
|
"abcdefghijklmn😀",
|
||||||
|
"🇮🇳 india mesh",
|
||||||
|
"👍🏽 thumbs up all round",
|
||||||
|
"plain long nickname here"
|
||||||
|
)
|
||||||
|
for (name in samples) {
|
||||||
|
for (limit in 1..20) {
|
||||||
|
val truncated = truncateNickname(name, maxLen = limit)
|
||||||
|
assertTrue("$name at $limit exceeded the limit", truncated.length <= limit)
|
||||||
|
assertTrue("$name at $limit is not a prefix of the name", name.startsWith(truncated))
|
||||||
|
assertTrue(
|
||||||
|
"$name at $limit ended in a lone surrogate",
|
||||||
|
truncated.isEmpty() || !Character.isHighSurrogate(truncated.last())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user