diff --git a/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt b/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt index 96ef57d4..8dfb87b0 100644 --- a/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt +++ b/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt @@ -41,10 +41,11 @@ data class BitchatPalette( val accentPurple: Color, // MARK: - Deterministic peer colors - /** Chroma applied after deriving a peer's stable hue. */ - val peerColorSaturation: Float, - /** Brightness applied after deriving a peer's stable hue. */ - val peerColorValue: Float, + /** + * Saturation/value applied after deriving a peer's stable hue. Swap this when adding a + * new theme — see [PeerColorStyle] for contrast guidelines. + */ + val peerColors: PeerColorStyle, ) val DarkBitchatPalette = BitchatPalette( @@ -56,8 +57,7 @@ val DarkBitchatPalette = BitchatPalette( textTertiary = Color(0xFF6B776B), accentOrange = Color(0xFFFF9F0A), accentPurple = Color(0xFFBF5AF2), - peerColorSaturation = 1f, - peerColorValue = 1f, + peerColors = PeerColorStyle.Dark, ) val LightBitchatPalette = BitchatPalette( @@ -69,8 +69,7 @@ val LightBitchatPalette = BitchatPalette( textTertiary = Color(0xFF757F75), accentOrange = Color(0xFFFF9500), accentPurple = Color(0xFFAF52DE), - peerColorSaturation = 0.85f, - peerColorValue = 0.45f, + peerColors = PeerColorStyle.Light, ) val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette } diff --git a/app/src/main/java/com/bitchat/android/ui/theme/PeerColors.kt b/app/src/main/java/com/bitchat/android/ui/theme/PeerColors.kt index b0f1ed7d..168c9863 100644 --- a/app/src/main/java/com/bitchat/android/ui/theme/PeerColors.kt +++ b/app/src/main/java/com/bitchat/android/ui/theme/PeerColors.kt @@ -1,9 +1,36 @@ package com.bitchat.android.ui.theme +import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import com.bitchat.android.ui.PeerIdentity import kotlin.math.abs +/** + * Theme-specific chroma applied after a peer's stable hue is derived. + * + * Hue stays identity-stable across themes (and byte-identical to iOS). Only saturation + * and value change so peer labels remain readable on each background. + * + * Guidelines when adding a future theme: + * - Dim / dark backgrounds: keep [value] high so colors are not lost against the surface; + * prefer muted [saturation] over neon. + * - Light backgrounds: keep [value] moderate-low so colors are not blinding; avoid + * near-full saturation. + */ +@Immutable +data class PeerColorStyle( + val saturation: Float, + val value: Float, +) { + companion object { + /** Soft pastels that stay bright enough on near-black chat surfaces. */ + val Dark = PeerColorStyle(saturation = 0.55f, value = 0.82f) + + /** Deeper, less saturated tones that stay readable on near-white surfaces. */ + val Light = PeerColorStyle(saturation = 0.70f, value = 0.42f) + } +} + /** * The single identity-to-color boundary used by chat, people sheets, and mentions. * @@ -22,9 +49,10 @@ fun colorForPeer(identity: PeerIdentity, palette: BitchatPalette): Color { hue = (hue + 0.12) % 1.0 } + val style = palette.peerColors return Color.hsv( hue = (hue * 360).toFloat(), - saturation = palette.peerColorSaturation, - value = palette.peerColorValue + saturation = style.saturation, + value = style.value ) } diff --git a/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt b/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt index e64448c3..69693b7e 100644 --- a/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt @@ -14,6 +14,7 @@ import com.bitchat.android.ui.theme.LightBitchatColorScheme import com.bitchat.android.ui.theme.LightBitchatPalette import com.bitchat.android.ui.theme.MessageBodyTextStyle import com.bitchat.android.ui.theme.MessageSenderTextStyle +import com.bitchat.android.ui.theme.PeerColorStyle import com.bitchat.android.ui.theme.colorForPeer import java.text.SimpleDateFormat import java.util.Date @@ -444,8 +445,8 @@ class ChatUIUtilsTest { @Test fun `peer color hue is stable across light and dark, only chroma differs`() { - // Hue derivation must stay byte-identical to iOS; only saturation/value are tuned for - // the redesigned neutral message body. + // Hue derivation must stay byte-identical to iOS; only saturation/value are tuned per + // theme so dark mode stays muted-but-bright and light mode stays deep-but-readable. val identity = PeerIdentity.mesh("abc") val dark = colorForPeer(identity, DarkBitchatPalette) val light = colorForPeer(identity, LightBitchatPalette) @@ -456,10 +457,16 @@ class ChatUIUtilsTest { rgbToHsv(light.red, light.green, light.blue, lightHsv) assertEquals(darkHsv[0].toDouble(), lightHsv[0].toDouble(), 1.0) - assertEquals(1.0, darkHsv[1].toDouble(), 0.01) - assertEquals(1.0, darkHsv[2].toDouble(), 0.01) - assertEquals(0.85, lightHsv[1].toDouble(), 0.01) - assertEquals(0.45, lightHsv[2].toDouble(), 0.01) + assertEquals(PeerColorStyle.Dark.saturation.toDouble(), darkHsv[1].toDouble(), 0.01) + assertEquals(PeerColorStyle.Dark.value.toDouble(), darkHsv[2].toDouble(), 0.01) + assertEquals(PeerColorStyle.Light.saturation.toDouble(), lightHsv[1].toDouble(), 0.01) + assertEquals(PeerColorStyle.Light.value.toDouble(), lightHsv[2].toDouble(), 0.01) + // Dark theme: muted chroma, never dark (readable on near-black). + assertTrue(darkHsv[1] < 0.75f) + assertTrue(darkHsv[2] >= 0.75f) + // Light theme: avoid neon / near-white peer labels. + assertTrue(lightHsv[1] < 0.85f) + assertTrue(lightHsv[2] <= 0.55f) } @Test @@ -485,7 +492,7 @@ class ChatUIUtilsTest { assertEquals(Color(0xFFF5F5F5), DarkBitchatColorScheme.onSurface) assertTrue(LightBitchatColorScheme.onSurface != DarkBitchatColorScheme.onSurface) assertTrue( - LightBitchatPalette.peerColorValue != DarkBitchatPalette.peerColorValue + LightBitchatPalette.peerColors != DarkBitchatPalette.peerColors ) }