mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-24 15:11:13 +00:00
fix(android): preserve graphemes in compact badges (#109486)
* fix(android): preserve graphemes in compact badges * chore(android): satisfy native source gates * fix(android): keep compact badge casing stable
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -7426,7 +7426,7 @@ class NodeRuntime private constructor(
|
||||
.split(' ', '-', '_')
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { token -> token.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.mapNotNull { token -> token.uppercaseFirstGraphemeOrNull() }
|
||||
.joinToString("")
|
||||
return if (initials.isNotEmpty()) initials else "OC"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,14 @@ internal fun String.firstGraphemeOrNull(): String? {
|
||||
return if (end == BreakIterator.DONE) null else substring(0, end)
|
||||
}
|
||||
|
||||
internal fun String.uppercaseFirstGraphemeOrNull(): String? {
|
||||
val grapheme = firstGraphemeOrNull() ?: return null
|
||||
val firstCodePoint = grapheme.codePointAt(0)
|
||||
val uppercaseCodePoint = Character.toUpperCase(firstCodePoint)
|
||||
// Keep badge width stable while preserving the rest of the grapheme cluster.
|
||||
return String(Character.toChars(uppercaseCodePoint)) + grapheme.substring(Character.charCount(firstCodePoint))
|
||||
}
|
||||
|
||||
internal fun String.takeUtf16Safe(maxChars: Int): String {
|
||||
if (length <= maxChars) return this
|
||||
// Keep the code-unit cap without leaving a high surrogate at its boundary.
|
||||
|
||||
@@ -12,6 +12,7 @@ import ai.openclaw.app.ui.design.ClawStatus
|
||||
import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawTextBadge
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import ai.openclaw.app.uppercaseFirstGraphemeOrNull
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -156,7 +157,7 @@ private fun channelBadge(label: String): String =
|
||||
.split(' ', '-', '_')
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.mapNotNull { it.uppercaseFirstGraphemeOrNull() }
|
||||
.joinToString("")
|
||||
.ifBlank { "C" }
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import ai.openclaw.app.ui.design.ClawStatus
|
||||
import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawTextBadge
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import ai.openclaw.app.uppercaseFirstGraphemeOrNull
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -340,7 +341,7 @@ private fun nodeBadge(value: String): String =
|
||||
.split(' ', '-', '_')
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.mapNotNull { it.uppercaseFirstGraphemeOrNull() }
|
||||
.joinToString("")
|
||||
.ifBlank { "N" }
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawScaffold
|
||||
import ai.openclaw.app.ui.design.ClawSecondaryButton
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import ai.openclaw.app.uppercaseFirstGraphemeOrNull
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -417,7 +418,7 @@ private fun providerInitials(value: String): String =
|
||||
.split(' ', '-', '_')
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.mapNotNull { it.uppercaseFirstGraphemeOrNull() }
|
||||
.joinToString("")
|
||||
.ifBlank { "AI" }
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ import ai.openclaw.app.ui.design.OpenClawMascot
|
||||
import ai.openclaw.app.ui.design.TalkWaveform
|
||||
import ai.openclaw.app.ui.design.TalkWaveformPhase
|
||||
import ai.openclaw.app.ui.design.agentAvatarSource
|
||||
import ai.openclaw.app.uppercaseFirstGraphemeOrNull
|
||||
import ai.openclaw.app.voice.VoiceWakePreferences
|
||||
import android.Manifest
|
||||
import android.content.ClipData
|
||||
@@ -2451,7 +2452,7 @@ private fun UsageProviderListRow(provider: GatewayUsageProviderSummary) {
|
||||
ClawDetailRow(
|
||||
title = provider.displayName,
|
||||
subtitle = usageProviderSubtitle(provider),
|
||||
leading = { ClawTextBadge(text = provider.displayName.firstOrNull()?.uppercase() ?: "U") },
|
||||
leading = { ClawTextBadge(text = provider.displayName.uppercaseFirstGraphemeOrNull() ?: "U") },
|
||||
trailing = { ClawStatusPill(text = if (hasIssue) nativeString("Issue") else "OK", status = if (hasIssue) ClawStatus.Warning else ClawStatus.Success) },
|
||||
)
|
||||
}
|
||||
@@ -2680,7 +2681,7 @@ private fun agentBadge(agent: GatewayAgentSummary): String {
|
||||
.split(' ', '-', '_')
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.mapNotNull { it.uppercaseFirstGraphemeOrNull() }
|
||||
.joinToString("")
|
||||
.ifBlank { "A" }
|
||||
}
|
||||
@@ -2937,7 +2938,7 @@ private fun notificationAppBadge(label: String): String {
|
||||
.asSequence()
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.mapNotNull { it.uppercaseFirstGraphemeOrNull() }
|
||||
.joinToString("")
|
||||
return initials.ifBlank { "A" }
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawTextBadge
|
||||
import ai.openclaw.app.ui.design.ClawTextField
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import ai.openclaw.app.uppercaseFirstGraphemeOrNull
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
@@ -893,6 +894,6 @@ private fun skillBadge(name: String): String =
|
||||
.split(' ', '-', '_')
|
||||
.filter { it.isNotBlank() }
|
||||
.take(2)
|
||||
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
|
||||
.mapNotNull { it.uppercaseFirstGraphemeOrNull() }
|
||||
.joinToString("")
|
||||
.ifBlank { "S" }
|
||||
|
||||
@@ -22,6 +22,18 @@ class Utf16TextTest {
|
||||
assertNull("".firstGraphemeOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uppercaseFirstGraphemeOrNullPreservesUserPerceivedCharacters() {
|
||||
assertEquals("🧭", "🧭 Scout".uppercaseFirstGraphemeOrNull())
|
||||
assertEquals("🇺🇸", "🇺🇸 Scout".uppercaseFirstGraphemeOrNull())
|
||||
assertEquals("👩🏽💻", "👩🏽💻 Dev".uppercaseFirstGraphemeOrNull())
|
||||
assertEquals("A\u0308", "a\u0308lice".uppercaseFirstGraphemeOrNull())
|
||||
assertEquals("S", "scout".uppercaseFirstGraphemeOrNull())
|
||||
assertEquals("ß", "ßcout".uppercaseFirstGraphemeOrNull())
|
||||
assertEquals("\uD801\uDC00", "\uD801\uDC28cout".uppercaseFirstGraphemeOrNull())
|
||||
assertNull("".uppercaseFirstGraphemeOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localizedInitialPreservesGraphemesAndLocale() {
|
||||
assertEquals("🧭", localizedInitial("🧭 Scout", languageTag = "en", fallbackLocale = Locale.US))
|
||||
|
||||
Reference in New Issue
Block a user