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:
parent
047e659719
commit
717a15bea5
3 changed files with 68 additions and 10 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 it’s 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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue