Skip to content

Commit 5e1bdbd

Browse files
authored
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.
1 parent beffba1 commit 5e1bdbd

File tree

3 files changed

+68
-10
lines changed

3 files changed

+68
-10
lines changed

libraries/designsystem/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ android {
3333
implementation(libs.vanniktech.blurhash)
3434
implementation(projects.features.enterprise.api)
3535
implementation(projects.libraries.architecture)
36+
implementation(projects.libraries.core)
3637
implementation(projects.libraries.preferences.api)
3738
implementation(projects.libraries.testtags)
3839
implementation(projects.libraries.uiStrings)

libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
package io.element.android.libraries.designsystem.components.avatar
99

1010
import androidx.compose.runtime.Immutable
11+
import io.element.android.libraries.core.data.tryOrNull
12+
import java.text.BreakIterator
1113

1214
@Immutable
1315
data class AvatarData(
@@ -27,24 +29,36 @@ data class AvatarData(
2729
startIndex++
2830
}
2931

30-
var length = 1
31-
var first = dn[startIndex]
32+
var next = dn[startIndex]
3233

3334
// LEFT-TO-RIGHT MARK
34-
if (dn.length >= 2 && 0x200e == first.code) {
35+
if (dn.length >= 2 && 0x200e == next.code) {
3536
startIndex++
36-
first = dn[startIndex]
37+
next = dn[startIndex]
3738
}
3839

39-
// check if it’s the start of a surrogate pair
40-
if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) {
41-
val second = dn[startIndex + 1]
42-
if (second.code in 0xDC00..0xDFFF) {
43-
length++
40+
while (next.isWhitespace()) {
41+
if (dn.length > startIndex + 1) {
42+
startIndex++
43+
next = dn[startIndex]
44+
} else {
45+
break
4446
}
4547
}
4648

47-
dn.substring(startIndex, startIndex + length)
49+
val fullCharacterIterator = BreakIterator.getCharacterInstance()
50+
fullCharacterIterator.setText(dn)
51+
val glyphBoundary = tryOrNull { fullCharacterIterator.following(startIndex) }
52+
?.takeIf { it in startIndex..dn.length }
53+
54+
when {
55+
// Use the found boundary
56+
glyphBoundary != null -> dn.substring(startIndex, glyphBoundary)
57+
// If no boundary was found, default to the next char if possible
58+
startIndex + 1 < dn.length -> dn.substring(startIndex, startIndex + 1)
59+
// Return a fallback character otherwise
60+
else -> "#"
61+
}
4862
}
4963
.uppercase()
5064
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.designsystem.components.avatar
9+
10+
import com.google.common.truth.Truth.assertThat
11+
import org.junit.Test
12+
13+
class AvatarDataTest {
14+
@Test
15+
fun `initial with text should get the first char, uppercased`() {
16+
val data = AvatarData("id", "test", null, AvatarSize.InviteSender)
17+
assertThat(data.initial).isEqualTo("T")
18+
}
19+
20+
@Test
21+
fun `initial with leading whitespace should get the first non-whitespace char, uppercased`() {
22+
val data = AvatarData("id", " test", null, AvatarSize.InviteSender)
23+
assertThat(data.initial).isEqualTo("T")
24+
}
25+
26+
@Test
27+
fun `initial with long emoji should get the full emoji`() {
28+
val data = AvatarData("id", "\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08 Test", null, AvatarSize.InviteSender)
29+
assertThat(data.initial).isEqualTo("\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08")
30+
}
31+
32+
@Test
33+
fun `initial with short emoji should get the emoji`() {
34+
val data = AvatarData("id", "✂ Test", null, AvatarSize.InviteSender)
35+
assertThat(data.initial).isEqualTo("")
36+
}
37+
38+
@Test
39+
fun `initial with a single letter should take that letter`() {
40+
val data = AvatarData("id", "T", null, AvatarSize.InviteSender)
41+
assertThat(data.initial).isEqualTo("T")
42+
}
43+
}

0 commit comments

Comments
 (0)