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(libs.vanniktech.blurhash)
implementation(projects.features.enterprise.api) implementation(projects.features.enterprise.api)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)

View file

@ -8,6 +8,8 @@
package io.element.android.libraries.designsystem.components.avatar package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.core.data.tryOrNull
import java.text.BreakIterator
@Immutable @Immutable
data class AvatarData( data class AvatarData(
@ -27,24 +29,36 @@ data class AvatarData(
startIndex++ startIndex++
} }
var length = 1 var next = dn[startIndex]
var first = dn[startIndex]
// LEFT-TO-RIGHT MARK // LEFT-TO-RIGHT MARK
if (dn.length >= 2 && 0x200e == first.code) { if (dn.length >= 2 && 0x200e == next.code) {
startIndex++ startIndex++
first = dn[startIndex] next = dn[startIndex]
} }
// check if its the start of a surrogate pair while (next.isWhitespace()) {
if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) { if (dn.length > startIndex + 1) {
val second = dn[startIndex + 1] startIndex++
if (second.code in 0xDC00..0xDFFF) { next = dn[startIndex]
length++ } 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() .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")
}
}