When an emoji is used as the 'initial' for an avatar, use the whole emoji (#4277)

* When an emoji is used as the 'initial' for an avatar, use the whole emoji

Use `BreakIterator.getCharacterInstance()` for a simpler solution.
This commit is contained in:
Jorge Martin Espinosa
2025-02-18 20:15:11 +01:00
committed by GitHub
parent 047e659719
commit 717a15bea5
3 changed files with 68 additions and 10 deletions

View File

@@ -33,6 +33,7 @@ android {
implementation(libs.vanniktech.blurhash)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)

View File

@@ -8,6 +8,8 @@
package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.runtime.Immutable
import io.element.android.libraries.core.data.tryOrNull
import java.text.BreakIterator
@Immutable
data class AvatarData(
@@ -27,24 +29,36 @@ data class AvatarData(
startIndex++
}
var length = 1
var first = dn[startIndex]
var next = dn[startIndex]
// LEFT-TO-RIGHT MARK
if (dn.length >= 2 && 0x200e == first.code) {
if (dn.length >= 2 && 0x200e == next.code) {
startIndex++
first = dn[startIndex]
next = dn[startIndex]
}
// check if its the start of a surrogate pair
if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) {
val second = dn[startIndex + 1]
if (second.code in 0xDC00..0xDFFF) {
length++
while (next.isWhitespace()) {
if (dn.length > startIndex + 1) {
startIndex++
next = dn[startIndex]
} else {
break
}
}
dn.substring(startIndex, startIndex + length)
val fullCharacterIterator = BreakIterator.getCharacterInstance()
fullCharacterIterator.setText(dn)
val glyphBoundary = tryOrNull { fullCharacterIterator.following(startIndex) }
?.takeIf { it in startIndex..dn.length }
when {
// Use the found boundary
glyphBoundary != null -> dn.substring(startIndex, glyphBoundary)
// If no boundary was found, default to the next char if possible
startIndex + 1 < dn.length -> dn.substring(startIndex, startIndex + 1)
// Return a fallback character otherwise
else -> "#"
}
}
.uppercase()
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.components.avatar
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class AvatarDataTest {
@Test
fun `initial with text should get the first char, uppercased`() {
val data = AvatarData("id", "test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("T")
}
@Test
fun `initial with leading whitespace should get the first non-whitespace char, uppercased`() {
val data = AvatarData("id", " test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("T")
}
@Test
fun `initial with long emoji should get the full emoji`() {
val data = AvatarData("id", "\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08 Test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08")
}
@Test
fun `initial with short emoji should get the emoji`() {
val data = AvatarData("id", "✂ Test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("")
}
@Test
fun `initial with a single letter should take that letter`() {
val data = AvatarData("id", "T", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("T")
}
}