This commit is contained in:
callebtc 2026-07-27 16:06:22 +02:00
parent c1c7704c32
commit 25e9779f73
27 changed files with 415 additions and 381 deletions

View File

@ -6,26 +6,26 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bitchat.android.R import com.bitchat.android.R
import com.bitchat.android.ui.theme.LocalBitchatPalette
@Composable @Composable
fun CloseButton( fun CloseButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
IconButton( IconButton(
onClick = onClick, onClick = onClick,
// 44.dp to match every other tap target in the app's chrome. // 44.dp to match every other tap target in the app's chrome.
modifier = modifier.size(44.dp), modifier = modifier.size(44.dp),
colors = IconButtonDefaults.iconButtonColors( colors = IconButtonDefaults.iconButtonColors(
contentColor = palette.accentGreen, contentColor = colorScheme.primary,
containerColor = Color.Transparent containerColor = Color.Transparent
) )
) { ) {

View File

@ -120,7 +120,6 @@ internal fun SheetIconSectionHeader(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
Column( Column(
modifier = modifier modifier = modifier
@ -152,7 +151,7 @@ internal fun SheetIconSectionHeader(
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 17.sp, lineHeight = 17.sp,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
} }
} }
@ -161,11 +160,11 @@ internal fun SheetIconSectionHeader(
/** Inset divider used inside grouped sheet cards (aligns with text column after the leading slot). */ /** Inset divider used inside grouped sheet cards (aligns with text column after the leading slot). */
@Composable @Composable
internal fun SheetCardDivider() { internal fun SheetCardDivider() {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(start = SheetRowDividerInset), modifier = Modifier.padding(start = SheetRowDividerInset),
thickness = 1.dp, thickness = 1.dp,
color = palette.outlineVariant color = colorScheme.outlineVariant
) )
} }
@ -212,7 +211,7 @@ internal fun AboutHero(
text = stringResource(R.string.about_tagline), text = stringResource(R.string.about_tagline),
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 16.sp, fontSize = 16.sp,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
@ -238,7 +237,6 @@ internal fun AboutTabBar(
onSelect: (AboutTab) -> Unit, onSelect: (AboutTab) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val density = LocalDensity.current val density = LocalDensity.current
@ -274,7 +272,7 @@ internal fun AboutTabBar(
} }
Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant) HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
Box( Box(
modifier = Modifier modifier = Modifier
// Lambda overload: the offset is animated every frame, and the non-lambda // Lambda overload: the offset is animated every frame, and the non-lambda
@ -295,11 +293,10 @@ private fun AboutTabLabel(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val color by animateColorAsState( val color by animateColorAsState(
targetValue = if (isSelected) colorScheme.primary else palette.textSecondary, targetValue = if (isSelected) colorScheme.primary else colorScheme.onSurfaceVariant,
animationSpec = tween(BitchatMotion.QUICK_MS, easing = FastOutSlowInEasing), animationSpec = tween(BitchatMotion.QUICK_MS, easing = FastOutSlowInEasing),
label = "aboutTabLabelColor" label = "aboutTabLabelColor"
) )
@ -328,7 +325,6 @@ private fun AboutInstructionRow(
text: String text: String
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
Row( Row(
modifier = Modifier modifier = Modifier
@ -350,7 +346,7 @@ private fun AboutInstructionRow(
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
color = palette.textPrimary color = colorScheme.onSurface
) )
} }
} }
@ -453,7 +449,6 @@ private fun AboutFeatureRow(
subtitle: String subtitle: String
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
Row( Row(
modifier = Modifier modifier = Modifier
@ -477,14 +472,14 @@ private fun AboutFeatureRow(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
lineHeight = 20.sp, lineHeight = 20.sp,
color = palette.textPrimary color = colorScheme.onSurface
) )
Text( Text(
text = subtitle, text = subtitle,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 17.sp, lineHeight = 17.sp,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
} }
} }
@ -498,10 +493,10 @@ internal fun BitchatBadge(
text: String, text: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
Box( Box(
modifier = modifier modifier = modifier
.background(palette.accentGreen.copy(alpha = 0.15f), RoundedCornerShape(4.dp)) .background(colorScheme.primary.copy(alpha = 0.15f), RoundedCornerShape(4.dp))
.padding(horizontal = 5.dp, vertical = 2.dp) .padding(horizontal = 5.dp, vertical = 2.dp)
) { ) {
Text( Text(
@ -510,7 +505,7 @@ internal fun BitchatBadge(
fontSize = 10.sp, fontSize = 10.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
color = palette.accentGreen color = colorScheme.primary
) )
} }
} }
@ -556,11 +551,10 @@ internal fun SheetHeaderBadge(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
Box( Box(
modifier = modifier modifier = modifier
.size(44.dp) .size(44.dp)
.background(palette.surface, androidx.compose.foundation.shape.CircleShape), .background(colorScheme.surface, androidx.compose.foundation.shape.CircleShape),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
@ -585,8 +579,8 @@ internal fun SheetDestructiveButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDestructive: Boolean = true isDestructive: Boolean = true
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
val accent = if (isDestructive) palette.accentRed else palette.accentGreen val accent = if (isDestructive) colorScheme.error else colorScheme.primary
Surface( Surface(
onClick = onClick, onClick = onClick,

View File

@ -82,17 +82,17 @@ private fun ThemeChip(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
// Cross-fade the chip so switching theme does not read as two separate flashes (the chip // Cross-fade the chip so switching theme does not read as two separate flashes (the chip
// recolouring plus the whole app recolouring underneath it). // recolouring plus the whole app recolouring underneath it).
val containerColor by animateColorAsState( val containerColor by animateColorAsState(
targetValue = if (selected) palette.accentGreen else palette.surfaceVariant, targetValue = if (selected) colorScheme.primary else colorScheme.surfaceVariant,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing), animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "themeChipContainer" label = "themeChipContainer"
) )
val labelColor by animateColorAsState( val labelColor by animateColorAsState(
targetValue = if (selected) Color.White else palette.textSecondary, targetValue = if (selected) Color.White else colorScheme.onSurfaceVariant,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing), animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "themeChipLabel" label = "themeChipLabel"
) )
@ -146,12 +146,12 @@ private fun SettingsToggleRow(
label = "settingsRowIcon" label = "settingsRowIcon"
) )
val titleColor by animateColorAsState( val titleColor by animateColorAsState(
targetValue = if (enabled) palette.textPrimary else palette.textTertiary, targetValue = if (enabled) colorScheme.onSurface else palette.textTertiary,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing), animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "settingsRowTitle" label = "settingsRowTitle"
) )
val subtitleColor by animateColorAsState( val subtitleColor by animateColorAsState(
targetValue = if (enabled) palette.textSecondary else palette.textTertiary, targetValue = if (enabled) colorScheme.onSurfaceVariant else palette.textTertiary,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing), animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "settingsRowSubtitle" label = "settingsRowSubtitle"
) )
@ -213,9 +213,9 @@ private fun SettingsToggleRow(
interactionSource = interactionSource, interactionSource = interactionSource,
colors = SwitchDefaults.colors( colors = SwitchDefaults.colors(
checkedThumbColor = Color.White, checkedThumbColor = Color.White,
checkedTrackColor = palette.accentGreen, checkedTrackColor = colorScheme.primary,
uncheckedThumbColor = Color.White, uncheckedThumbColor = Color.White,
uncheckedTrackColor = palette.surfaceVariant uncheckedTrackColor = colorScheme.surfaceVariant
) )
) )
} }
@ -310,7 +310,7 @@ fun AboutSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Row( Row(
@ -359,7 +359,7 @@ fun AboutSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Column { Column {
@ -383,7 +383,7 @@ fun AboutSheet(
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(start = 54.dp), modifier = Modifier.padding(start = 54.dp),
thickness = 1.dp, thickness = 1.dp,
color = palette.outlineVariant color = colorScheme.outlineVariant
) )
// Proof of Work Toggle // Proof of Work Toggle
@ -398,7 +398,7 @@ fun AboutSheet(
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(start = 54.dp), modifier = Modifier.padding(start = 54.dp),
thickness = 1.dp, thickness = 1.dp,
color = palette.outlineVariant color = colorScheme.outlineVariant
) )
// Tor Toggle // Tor Toggle
@ -417,9 +417,9 @@ fun AboutSheet(
statusIndicator = if (torMode.value == TorMode.ON) { statusIndicator = if (torMode.value == TorMode.ON) {
{ {
val statusColor = when { val statusColor = when {
torStatus.running && torStatus.bootstrapPercent >= 100 -> palette.accentGreen torStatus.running && torStatus.bootstrapPercent >= 100 -> colorScheme.primary
torStatus.running -> palette.accentOrange torStatus.running -> palette.accentOrange
else -> palette.accentRed else -> colorScheme.error
} }
Surface( Surface(
color = statusColor, color = statusColor,
@ -811,7 +811,7 @@ fun AboutSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Column( Column(
@ -828,7 +828,7 @@ fun AboutSheet(
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = palette.textPrimary color = colorScheme.onSurface
) )
AnimatedCountLabel( AnimatedCountLabel(
count = powDifficulty, count = powDifficulty,
@ -839,7 +839,7 @@ fun AboutSheet(
), ),
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 12.sp, fontSize = 12.sp,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
} }
@ -849,9 +849,9 @@ fun AboutSheet(
valueRange = 0f..32f, valueRange = 0f..32f,
steps = 31, steps = 31,
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
thumbColor = palette.accentGreen, thumbColor = colorScheme.primary,
activeTrackColor = palette.accentGreen, activeTrackColor = colorScheme.primary,
inactiveTrackColor = palette.surfaceVariant inactiveTrackColor = colorScheme.surfaceVariant
) )
) )
@ -888,7 +888,7 @@ fun AboutSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Column( Column(
@ -900,9 +900,9 @@ fun AboutSheet(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
val statusColor = when { val statusColor = when {
torStatus.running && torStatus.bootstrapPercent >= 100 -> palette.accentGreen torStatus.running && torStatus.bootstrapPercent >= 100 -> colorScheme.primary
torStatus.running -> palette.accentOrange torStatus.running -> palette.accentOrange
else -> palette.accentRed else -> colorScheme.error
} }
Surface(color = statusColor, shape = CircleShape, modifier = Modifier.size(10.dp)) {} Surface(color = statusColor, shape = CircleShape, modifier = Modifier.size(10.dp)) {}
Text( Text(
@ -914,7 +914,7 @@ fun AboutSheet(
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = palette.textPrimary color = colorScheme.onSurface
) )
} }
if (torStatus.lastLogLine.isNotEmpty()) { if (torStatus.lastLogLine.isNotEmpty()) {
@ -974,7 +974,7 @@ fun AboutSheet(
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.fillMaxWidth() .fillMaxWidth()
.height(64.dp) .height(64.dp)
.background(palette.background.copy(alpha = topBarAlpha)) .background(colorScheme.background.copy(alpha = topBarAlpha))
) { ) {
val dismiss = LocalSheetDismiss.current val dismiss = LocalSheetDismiss.current
CloseButton( CloseButton(

View File

@ -119,11 +119,12 @@ internal data class TorConnectionVisual(
@Composable @Composable
internal fun rememberTorConnectionVisual(normal: Color): TorConnectionVisual { internal fun rememberTorConnectionVisual(normal: Color): TorConnectionVisual {
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme
val torStatus by remember { ArtiTorManager.getInstance() }.statusFlow.collectAsState() val torStatus by remember { ArtiTorManager.getInstance() }.statusFlow.collectAsState()
// ~28% of the loud accent mixed into the base tint keeps the hue without intensity. // ~28% of the loud accent mixed into the base tint keeps the hue without intensity.
val mutedConnecting = lerp(normal, palette.accentOrange, 0.28f) val mutedConnecting = lerp(normal, palette.accentOrange, 0.28f)
val mutedFailed = lerp(normal, palette.accentRed, 0.30f) val mutedFailed = lerp(normal, colorScheme.error, 0.30f)
val target = when { val target = when {
torStatus.mode == TorMode.OFF -> TorConnectionVisual(normal, isProgress = false) torStatus.mode == TorMode.OFF -> TorConnectionVisual(normal, isProgress = false)
@ -268,18 +269,19 @@ fun NoiseSessionIcon(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme
// The pre-redesign colours for the first two states were `0x87878700`, i.e. alpha 0x87 with // The pre-redesign colours for the first two states were `0x87878700`, i.e. alpha 0x87 with
// an all-but-transparent RGB - the icons were effectively invisible. They now use the // an all-but-transparent RGB - the icons were effectively invisible. They now use the
// palette's secondary text colour. // palette's secondary text colour.
val (icon, color, contentDescription) = when (sessionState) { val (icon, color, contentDescription) = when (sessionState) {
"uninitialized" -> Triple( "uninitialized" -> Triple(
Icons.Outlined.NoEncryption, Icons.Outlined.NoEncryption,
palette.textSecondary, colorScheme.onSurfaceVariant,
stringResource(R.string.cd_ready_for_handshake) stringResource(R.string.cd_ready_for_handshake)
) )
"handshaking" -> Triple( "handshaking" -> Triple(
Icons.Outlined.Sync, Icons.Outlined.Sync,
palette.textSecondary, colorScheme.onSurfaceVariant,
stringResource(R.string.cd_handshake_in_progress) stringResource(R.string.cd_handshake_in_progress)
) )
"established" -> Triple( "established" -> Triple(
@ -290,7 +292,7 @@ fun NoiseSessionIcon(
else -> { // "failed" or any other state else -> { // "failed" or any other state
Triple( Triple(
Icons.Outlined.Warning, Icons.Outlined.Warning,
palette.accentRed, colorScheme.error,
stringResource(R.string.cd_handshake_failed) stringResource(R.string.cd_handshake_failed)
) )
} }
@ -365,19 +367,20 @@ fun PeerCounter(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme
// Compute channel-aware people count and color (matches iOS logic exactly) // Compute channel-aware people count and color (matches iOS logic exactly)
val (peopleCount, countColor) = when (selectedLocationChannel) { val (peopleCount, countColor) = when (selectedLocationChannel) {
is com.bitchat.android.geohash.ChannelID.Location -> { is com.bitchat.android.geohash.ChannelID.Location -> {
// Geohash channel: show geohash participants // Geohash channel: show geohash participants
val count = geohashPeople.size val count = geohashPeople.size
Pair(count, if (count > 0) palette.accentGreen else palette.textTertiary) Pair(count, if (count > 0) colorScheme.primary else palette.textTertiary)
} }
is com.bitchat.android.geohash.ChannelID.Mesh, is com.bitchat.android.geohash.ChannelID.Mesh,
null -> { null -> {
// Mesh channel: show Bluetooth-connected peers (excluding self) // Mesh channel: show Bluetooth-connected peers (excluding self)
val count = connectedPeers.size val count = connectedPeers.size
Pair(count, if (isConnected && count > 0) palette.accentBlue else palette.textTertiary) Pair(count, if (isConnected && count > 0) colorScheme.secondary else palette.textTertiary)
} }
} }
@ -424,7 +427,7 @@ fun PeerCounter(
prefix = stringResource(R.string.channel_count_prefix), prefix = stringResource(R.string.channel_count_prefix),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontSize = HeaderTextSize, fontSize = HeaderTextSize,
color = if (isConnected) palette.accentGreen else palette.accentRed, color = if (isConnected) colorScheme.primary else colorScheme.error,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
} }
@ -482,7 +485,6 @@ private fun ChannelHeader(
onSidebarClick: () -> Unit onSidebarClick: () -> Unit
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
// Back: a chevron alone is unambiguous here and buys back ~40.dp of title space that // Back: a chevron alone is unambiguous here and buys back ~40.dp of title space that
@ -520,7 +522,7 @@ private fun ChannelHeader(
text = stringResource(R.string.chat_leave), text = stringResource(R.string.chat_leave),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
fontSize = 15.sp, fontSize = 15.sp,
color = palette.accentRed, color = colorScheme.error,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
.clip(HeaderClusterShape) .clip(HeaderClusterShape)
@ -667,7 +669,7 @@ private fun LocationChannelsButton(
viewModel: ChatViewModel, viewModel: ChatViewModel,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
// Get current channel selection from location manager // Get current channel selection from location manager
val selectedChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle() val selectedChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
@ -680,7 +682,7 @@ private fun LocationChannelsButton(
// so it is plain "mesh". // so it is plain "mesh".
else -> stringResource(R.string.mesh_label) else -> stringResource(R.string.mesh_label)
} }
val channelColor = if (isLocation) palette.accentGreen else palette.accentBlue val channelColor = if (isLocation) colorScheme.primary else colorScheme.secondary
// Tor status only tints the globe (location channels). The local mesh stays blue. // Tor status only tints the globe (location channels). The local mesh stays blue.
val torVisual = if (isLocation) { val torVisual = if (isLocation) {
rememberTorConnectionVisual(normal = channelColor) rememberTorConnectionVisual(normal = channelColor)

View File

@ -46,7 +46,6 @@ import com.bitchat.android.nostr.LocationNotesManager
import com.bitchat.android.nostr.NearbyNotesController import com.bitchat.android.nostr.NearbyNotesController
import com.bitchat.android.ui.media.FullScreenImageViewer import com.bitchat.android.ui.media.FullScreenImageViewer
import com.bitchat.android.ui.theme.BitchatMotion import com.bitchat.android.ui.theme.BitchatMotion
import com.bitchat.android.ui.theme.LocalBitchatPalette
/** /**
* Main ChatScreen - REFACTORED to use component-based architecture * Main ChatScreen - REFACTORED to use component-based architecture
@ -61,7 +60,6 @@ import com.bitchat.android.ui.theme.LocalBitchatPalette
@Composable @Composable
fun ChatScreen(viewModel: ChatViewModel) { fun ChatScreen(viewModel: ChatViewModel) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
val messages by viewModel.messages.collectAsStateWithLifecycle() val messages by viewModel.messages.collectAsStateWithLifecycle()
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle() val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
val nickname by viewModel.nickname.collectAsStateWithLifecycle() val nickname by viewModel.nickname.collectAsStateWithLifecycle()
@ -415,17 +413,17 @@ fun ChatScreen(viewModel: ChatViewModel) {
) { ) {
Surface( Surface(
shape = CircleShape, shape = CircleShape,
color = palette.surface, color = colorScheme.surface,
tonalElevation = 3.dp, tonalElevation = 3.dp,
shadowElevation = 6.dp, shadowElevation = 6.dp,
border = BorderStroke(1.dp, palette.accentGreen) border = BorderStroke(1.dp, colorScheme.primary)
) { ) {
IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) { IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) {
Icon( Icon(
imageVector = Icons.Filled.ArrowDownward, imageVector = Icons.Filled.ArrowDownward,
contentDescription = stringResource(com.bitchat.android.R.string.cd_scroll_to_bottom), contentDescription = stringResource(com.bitchat.android.R.string.cd_scroll_to_bottom),
modifier = Modifier.size(22.dp), modifier = Modifier.size(22.dp),
tint = palette.accentGreen tint = colorScheme.primary
) )
} }
} }
@ -569,8 +567,6 @@ fun ChatInputSection(
showMediaButtons: Boolean, showMediaButtons: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current
Column( Column(
// Flat, slightly translucent screen background — the same treatment as the top bar, so the // Flat, slightly translucent screen background — the same treatment as the top bar, so the
// two bars are visibly the same kind of surface. No gradient: a soft ramp here just looked // two bars are visibly the same kind of surface. No gradient: a soft ramp here just looked
@ -581,7 +577,7 @@ fun ChatInputSection(
.background(colorScheme.background.copy(alpha = BarBackgroundAlpha)) .background(colorScheme.background.copy(alpha = BarBackgroundAlpha))
) { ) {
// Hairline marking where chrome begins. Faint on purpose — it is a hint, not a border. // Hairline marking where chrome begins. Faint on purpose — it is a hint, not a border.
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant) HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
// Command suggestions box // Command suggestions box
if (showCommandSuggestions && commandSuggestions.isNotEmpty()) { if (showCommandSuggestions && commandSuggestions.isNotEmpty()) {
@ -590,7 +586,7 @@ fun ChatInputSection(
onSuggestionClick = onCommandSuggestionClick, onSuggestionClick = onCommandSuggestionClick,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant) HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
} }
// Retain the final populated list while the picker exits. The state layer clears // Retain the final populated list while the picker exits. The state layer clears
// suggestions together with visibility; without this snapshot the panel would empty and // suggestions together with visibility; without this snapshot the panel would empty and
@ -631,7 +627,7 @@ fun ChatInputSection(
onSuggestionClick = onMentionSuggestionClick, onSuggestionClick = onMentionSuggestionClick,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant) HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
} }
} }
MessageInput( MessageInput(
@ -682,7 +678,6 @@ private fun ChatFloatingHeader(
) { ) {
val context = androidx.compose.ui.platform.LocalContext.current val context = androidx.compose.ui.platform.LocalContext.current
val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) } val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) }
val palette = LocalBitchatPalette.current
Box( Box(
modifier = Modifier modifier = Modifier

View File

@ -10,6 +10,7 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.ui.theme.BASE_FONT_SIZE import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import com.bitchat.android.ui.theme.BitchatPalette import com.bitchat.android.ui.theme.BitchatPalette
import com.bitchat.android.ui.theme.ChatVisualTokens import com.bitchat.android.ui.theme.ChatVisualTokens
import com.bitchat.android.ui.theme.colorForPeerSeed
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -57,7 +58,11 @@ fun formatTextMessageSender(
): AnnotatedString { ): AnnotatedString {
val builder = AnnotatedString.Builder() val builder = AnnotatedString.Builder()
val isSelf = message.isFromSelf(currentUserNickname, myPeerID) val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
val senderColor = if (isSelf) palette.accentOrange else getPeerColor(message, palette.isDark) val senderColor = if (isSelf) {
palette.accentOrange
} else {
colorForPeerSeed(peerColorSeedForMessage(message), palette)
}
val senderWeight = FontWeight.SemiBold val senderWeight = FontWeight.SemiBold
val (baseName, suffix) = splitSuffix(message.sender) val (baseName, suffix) = splitSuffix(message.sender)
@ -161,12 +166,12 @@ private fun appendBodyTimestamp(
private fun appendMutedTimestamp( private fun appendMutedTimestamp(
builder: AnnotatedString.Builder, builder: AnnotatedString.Builder,
message: BitchatMessage, message: BitchatMessage,
palette: BitchatPalette, contentColor: Color,
timeFormatter: SimpleDateFormat, timeFormatter: SimpleDateFormat,
) { ) {
builder.pushStyle( builder.pushStyle(
SpanStyle( SpanStyle(
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha), color = contentColor.copy(alpha = ChatVisualTokens.MutedTextAlpha),
fontSize = ChatVisualTokens.SystemTimeFontSize, fontSize = ChatVisualTokens.SystemTimeFontSize,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
) )
@ -186,6 +191,8 @@ fun formatTextMessageBody(
message: BitchatMessage, message: BitchatMessage,
currentUserNickname: String, currentUserNickname: String,
palette: BitchatPalette, palette: BitchatPalette,
contentColor: Color,
linkColor: Color,
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()), timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
includeTimestamp: Boolean = true includeTimestamp: Boolean = true
): AnnotatedString { ): AnnotatedString {
@ -196,6 +203,8 @@ fun formatTextMessageBody(
content = message.content, content = message.content,
currentUserNickname = currentUserNickname, currentUserNickname = currentUserNickname,
palette = palette, palette = palette,
contentColor = contentColor,
linkColor = linkColor,
) )
if (includeTimestamp) { if (includeTimestamp) {
@ -212,13 +221,13 @@ fun formatTextMessageBody(
*/ */
fun formatSystemMessage( fun formatSystemMessage(
message: BitchatMessage, message: BitchatMessage,
palette: BitchatPalette, contentColor: Color,
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()) timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault())
): AnnotatedString { ): AnnotatedString {
val builder = AnnotatedString.Builder() val builder = AnnotatedString.Builder()
builder.pushStyle( builder.pushStyle(
SpanStyle( SpanStyle(
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha), color = contentColor.copy(alpha = ChatVisualTokens.MutedTextAlpha),
fontSize = ChatVisualTokens.SystemActionFontSize, fontSize = ChatVisualTokens.SystemActionFontSize,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
) )
@ -227,7 +236,7 @@ fun formatSystemMessage(
builder.append(message.content) builder.append(message.content)
builder.pop() builder.pop()
appendMutedTimestamp(builder, message, palette, timeFormatter) appendMutedTimestamp(builder, message, contentColor, timeFormatter)
return builder.toAnnotatedString() return builder.toAnnotatedString()
} }
@ -243,6 +252,7 @@ fun formatMessageHeaderAnnotatedString(
currentUserNickname: String, currentUserNickname: String,
myPeerID: String, myPeerID: String,
palette: BitchatPalette, palette: BitchatPalette,
contentColor: Color,
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()), timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
includeSender: Boolean = true includeSender: Boolean = true
): AnnotatedString { ): AnnotatedString {
@ -250,11 +260,15 @@ fun formatMessageHeaderAnnotatedString(
val isSelf = message.isFromSelf(currentUserNickname, myPeerID) val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
if (message.sender == "system") { if (message.sender == "system") {
return formatSystemMessage(message, palette, timeFormatter) return formatSystemMessage(message, contentColor, timeFormatter)
} }
if (includeSender) { if (includeSender) {
val baseColor = if (isSelf) palette.accentOrange else getPeerColor(message, palette.isDark) val baseColor = if (isSelf) {
palette.accentOrange
} else {
colorForPeerSeed(peerColorSeedForMessage(message), palette)
}
val (baseName, suffix) = splitSuffix(message.sender) val (baseName, suffix) = splitSuffix(message.sender)
builder.pushStyle( builder.pushStyle(
@ -291,71 +305,10 @@ fun formatMessageHeaderAnnotatedString(
} }
} }
appendMutedTimestamp(builder, message, palette, timeFormatter) appendMutedTimestamp(builder, message, contentColor, timeFormatter)
return builder.toAnnotatedString() return builder.toAnnotatedString()
} }
/**
* iOS-style peer color assignment using djb2 hash algorithm
* Avoids orange (~30°) reserved for self messages
*/
fun getPeerColor(message: BitchatMessage, isDark: Boolean): Color {
// Create seed from peer identifier (prioritizing stable keys)
val seed = when {
message.senderPeerID?.startsWith("nostr:") == true || message.senderPeerID?.startsWith("nostr_") == true -> {
// For Nostr peers, use the full key if available, otherwise the peer ID
"nostr:${message.senderPeerID.lowercase()}"
}
message.senderPeerID?.length == 16 -> {
// For ephemeral peer IDs, try to get stable Noise key, fallback to peer ID
"noise:${message.senderPeerID.lowercase()}"
}
message.senderPeerID?.length == 64 -> {
// This is already a stable Noise key
"noise:${message.senderPeerID.lowercase()}"
}
else -> {
// Fallback to sender name
message.sender.lowercase()
}
}
return colorForPeerSeed(seed, isDark)
}
/**
* Generate consistent peer color using djb2 hash.
*
* The hash and hue derivation are byte-identical to the iOS implementation, so a given peer
* resolves to the same hue on both platforms. Saturation and value are deliberately higher
* than iOS: the redesigned chat surface renders message bodies in neutral near-white, so
* nicknames need more chroma to stay distinguishable at a glance.
*/
fun colorForPeerSeed(seed: String, isDark: Boolean): Color {
// djb2 hash algorithm (matches iOS implementation)
var hash = 5381UL
for (byte in seed.toByteArray()) {
hash = ((hash shl 5) + hash) + byte.toUByte().toULong()
}
var hue = (hash % 360UL).toDouble() / 360.0
// Avoid orange (~30°) reserved for self (matches iOS logic)
val orange = 30.0 / 360.0
if (kotlin.math.abs(hue - orange) < 0.05) {
hue = (hue + 0.12) % 1.0
}
val saturation = if (isDark) 1.0 else 0.85
val brightness = if (isDark) 1.0 else 0.45
return Color.hsv(
hue = (hue * 360).toFloat(),
saturation = saturation.toFloat(),
value = brightness.toFloat()
)
}
/** /**
* Split a name into base and a '#abcd' suffix if present (matches iOS splitSuffix exactly) * Split a name into base and a '#abcd' suffix if present (matches iOS splitSuffix exactly)
*/ */
@ -387,7 +340,7 @@ internal fun isUnannouncedNickname(displayName: String): Boolean {
* iOS-style content formatting with proper hashtag and mention handling. * iOS-style content formatting with proper hashtag and mention handling.
* *
* Redesign notes: * Redesign notes:
* - Plain text renders in [BitchatPalette.textPrimary]; colour is reserved for `@mentions`, * - Plain text renders in Material `onSurface`; colour is reserved for `@mentions`,
* links and geohashes. * links and geohashes.
* - Mentions get a tinted background chip so they read as a distinct token inside a sentence. * - Mentions get a tinted background chip so they read as a distinct token inside a sentence.
* The chip is tinted by *the mentioned peer's* colour, not the sender's, so `@alice` looks * The chip is tinted by *the mentioned peer's* colour, not the sender's, so `@alice` looks
@ -401,10 +354,9 @@ private fun appendIOSFormattedContent(
content: String, content: String,
currentUserNickname: String, currentUserNickname: String,
palette: BitchatPalette, palette: BitchatPalette,
contentColor: Color,
linkColor: Color,
) { ) {
val contentColor = palette.textPrimary
val linkColor = palette.accentBlue
// iOS-style patterns: allow optional '#abcd' suffix in mentions // iOS-style patterns: allow optional '#abcd' suffix in mentions
val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex() val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex()
val mentionPattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)".toRegex() val mentionPattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)".toRegex()
@ -511,7 +463,10 @@ private fun appendIOSFormattedContent(
palette.accentOrange palette.accentOrange
} else { } else {
// Tint by the *mentioned* peer so a given name looks identical everywhere. // Tint by the *mentioned* peer so a given name looks identical everywhere.
colorForPeerSeed(mentionWithoutAt.lowercase(), palette.isDark) colorForPeerSeed(
PeerColorSeed(mentionWithoutAt.lowercase(Locale.ROOT)),
palette
)
} }
val chipAlpha = if (isMentionToMe) MENTION_CHIP_ALPHA_SELF else MENTION_CHIP_ALPHA val chipAlpha = if (isMentionToMe) MENTION_CHIP_ALPHA_SELF else MENTION_CHIP_ALPHA
val mentionWeight = if (isMentionToMe) FontWeight.Bold else FontWeight.SemiBold val mentionWeight = if (isMentionToMe) FontWeight.Bold else FontWeight.SemiBold

View File

@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BitchatFontFamily import com.bitchat.android.ui.theme.BitchatFontFamily
import com.bitchat.android.ui.theme.BASE_FONT_SIZE import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import com.bitchat.android.ui.theme.LocalBitchatPalette
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.bitchat.android.R import com.bitchat.android.R
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
@ -35,14 +36,13 @@ fun ChatUserSheet(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
// iOS system colors (matches LocationChannelsSheet exactly)
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f val palette = LocalBitchatPalette.current
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) // iOS green val standardGreen = colorScheme.primary
val standardBlue = Color(0xFF007AFF) // iOS blue val standardBlue = colorScheme.secondary
val standardPurple = if (isDark) Color(0xFFBF5AF2) else Color(0xFFAF52DE) // iOS purple val standardPurple = palette.accentPurple
val standardRed = Color(0xFFFF3B30) // iOS red val standardRed = colorScheme.error
val standardGrey = if (isDark) Color(0xFF8E8E93) else Color(0xFF6D6D70) // iOS grey val standardGrey = colorScheme.onSurfaceVariant
if (isPresented) { if (isPresented) {
BitchatBottomSheet( BitchatBottomSheet(

View File

@ -1195,17 +1195,12 @@ class ChatViewModel(
/** /**
* Get consistent color for a mesh peer by ID (iOS-compatible) * Get consistent color for a mesh peer by ID (iOS-compatible)
*/ */
fun colorForMeshPeer(peerID: String, isDark: Boolean): androidx.compose.ui.graphics.Color { fun peerColorSeedForMeshPeer(peerID: String): PeerColorSeed = meshPeerColorSeed(peerID)
// Try to get stable Noise key, fallback to peer ID
val seed = "noise:${peerID.lowercase()}"
return colorForPeerSeed(seed, isDark).copy()
}
/** /**
* Get consistent color for a Nostr pubkey (iOS-compatible) * Get consistent color for a Nostr pubkey (iOS-compatible)
*/ */
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color { fun peerColorSeedForNostrPubkey(pubkeyHex: String): PeerColorSeed =
return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark) geohashViewModel.peerColorSeedForNostrPubkey(pubkeyHex)
}
} }

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.ui.theme.BitchatFontFamily import com.bitchat.android.ui.theme.BitchatFontFamily
import com.bitchat.android.ui.theme.colorForPeerSeed
import com.bitchat.android.R import com.bitchat.android.R
import com.bitchat.android.ui.theme.LocalBitchatPalette import com.bitchat.android.ui.theme.LocalBitchatPalette
import java.util.* import java.util.*
@ -45,6 +46,7 @@ fun GeohashPeopleList(
val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle() val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme
val myHex = remember(selectedLocationChannel) { val myHex = remember(selectedLocationChannel) {
when (val channel = selectedLocationChannel) { when (val channel = selectedLocationChannel) {
is com.bitchat.android.geohash.ChannelID.Location -> { is com.bitchat.android.geohash.ChannelID.Location -> {
@ -110,7 +112,7 @@ fun GeohashPeopleList(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding) .padding(horizontal = AboutHorizontalPadding)
.padding(top = 10.dp), .padding(top = 10.dp),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Text( Text(
@ -212,14 +214,14 @@ private fun PeopleCard(
people: List<GeoPerson>, people: List<GeoPerson>,
row: @Composable (GeoPerson) -> Unit row: @Composable (GeoPerson) -> Unit
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding) .padding(horizontal = AboutHorizontalPadding)
.padding(top = 10.dp), .padding(top = 10.dp),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
AnimatedRowColumn(items = people, key = { it.id }) { index, person -> AnimatedRowColumn(items = people, key = { it.id }) { index, person ->
@ -250,7 +252,10 @@ private fun GeohashPersonItem(
val (baseNameRaw, suffixRaw) = splitSuffix(person.displayName) val (baseNameRaw, suffixRaw) = splitSuffix(person.displayName)
val baseName = truncateNickname(baseNameRaw) val baseName = truncateNickname(baseNameRaw)
val suffix = if (showHashSuffix) suffixRaw else "" val suffix = if (showHashSuffix) suffixRaw else ""
val assignedColor = viewModel.colorForNostrPubkey(person.id, palette.isDark) val assignedColor = colorForPeerSeed(
viewModel.peerColorSeedForNostrPubkey(person.id),
palette
)
val baseColor = if (isMe) palette.accentOrange else assignedColor val baseColor = if (isMe) palette.accentOrange else assignedColor
Row( Row(

View File

@ -30,10 +30,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bitchat.android.ui.theme.BitchatTheme
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -87,15 +87,13 @@ class GeohashPickerActivity : OrientationAwareActivity() {
val initialPrecision = geohashToFocus?.length ?: 5 val initialPrecision = geohashToFocus?.length ?: 5
setContent { setContent {
MaterialTheme { BitchatTheme {
var currentGeohash by remember { mutableStateOf(geohashToFocus ?: "") } var currentGeohash by remember { mutableStateOf(geohashToFocus ?: "") }
var precision by remember { mutableStateOf(initialPrecision.coerceIn(1, 12)) } var precision by remember { mutableStateOf(initialPrecision.coerceIn(1, 12)) }
var webViewRef by remember { mutableStateOf<WebView?>(null) } var webViewRef by remember { mutableStateOf<WebView?>(null) }
// iOS system-like colors used across app
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f val standardGreen = colorScheme.primary
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
Scaffold { padding -> Scaffold { padding ->
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {

View File

@ -332,10 +332,8 @@ class GeohashViewModel(
fun displayNameForNostrPubkeyUI(pubkeyHex: String): String = repo.displayNameForNostrPubkeyUI(pubkeyHex) fun displayNameForNostrPubkeyUI(pubkeyHex: String): String = repo.displayNameForNostrPubkeyUI(pubkeyHex)
fun displayNameForGeohashConversation(pubkeyHex: String, sourceGeohash: String): String = repo.displayNameForGeohashConversation(pubkeyHex, sourceGeohash) fun displayNameForGeohashConversation(pubkeyHex: String, sourceGeohash: String): String = repo.displayNameForGeohashConversation(pubkeyHex, sourceGeohash)
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color { fun peerColorSeedForNostrPubkey(pubkeyHex: String): PeerColorSeed =
val seed = "nostr:${pubkeyHex.lowercase()}" nostrPeerColorSeed(pubkeyHex)
return colorForPeerSeed(seed, isDark).copy()
}
private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) { private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
geoTimer?.cancel(); geoTimer = null geoTimer?.cancel(); geoTimer = null

View File

@ -257,7 +257,8 @@ internal fun ComposerActionSurface(
content: @Composable (tint: Color) -> Unit content: @Composable (tint: Color) -> Unit
) { ) {
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val accent = if (activeColor == Color.Unspecified) palette.accentGreen else activeColor val colorScheme = MaterialTheme.colorScheme
val accent = if (activeColor == Color.Unspecified) colorScheme.primary else activeColor
val container by animateColorAsState( val container by animateColorAsState(
// A tint rather than a fill. A solid accent disc next to the text you are typing was the // A tint rather than a fill. A solid accent disc next to the text you are typing was the
@ -267,7 +268,7 @@ internal fun ComposerActionSurface(
label = "composerButtonContainer" label = "composerButtonContainer"
) )
val tint by animateColorAsState( val tint by animateColorAsState(
targetValue = if (isActive) accent else palette.textSecondary, targetValue = if (isActive) accent else colorScheme.onSurfaceVariant,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing), animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "composerButtonTint" label = "composerButtonTint"
) )
@ -314,6 +315,7 @@ fun MessageInput(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme
val isFocused = remember { mutableStateOf(false) } val isFocused = remember { mutableStateOf(false) }
val hasText = value.text.isNotBlank() val hasText = value.text.isNotBlank()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -324,7 +326,7 @@ fun MessageInput(
// Recording is the one state worth shouting about, so it overrides focus. // Recording is the one state worth shouting about, so it overrides focus.
val borderColor by animateColorAsState( val borderColor by animateColorAsState(
targetValue = when { targetValue = when {
isRecording -> palette.accentRed.copy(alpha = 0.7f) isRecording -> colorScheme.error.copy(alpha = 0.7f)
isFocused.value -> palette.inputOutlineFocused isFocused.value -> palette.inputOutlineFocused
else -> palette.inputOutline else -> palette.inputOutline
}, },
@ -371,11 +373,11 @@ fun MessageInput(
// user is composing rather than reading, and green-on-black is tiring to // user is composing rather than reading, and green-on-black is tiring to
// type into. // type into.
textStyle = MaterialTheme.typography.bodyMedium.copy( textStyle = MaterialTheme.typography.bodyMedium.copy(
color = palette.textPrimary, color = colorScheme.onSurface,
fontFamily = BitchatFontFamily fontFamily = BitchatFontFamily
), ),
cursorBrush = SolidColor( cursorBrush = SolidColor(
if (isRecording) Color.Transparent else palette.textPrimary if (isRecording) Color.Transparent else colorScheme.onSurface
), ),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
keyboardActions = KeyboardActions(onSend = { keyboardActions = KeyboardActions(onSend = {
@ -387,8 +389,8 @@ fun MessageInput(
CombinedVisualTransformation( CombinedVisualTransformation(
listOf( listOf(
SlashCommandVisualTransformation( SlashCommandVisualTransformation(
commandColor = palette.accentGreen, commandColor = colorScheme.primary,
commandBackground = palette.accentGreen.copy(alpha = 0.14f), commandBackground = colorScheme.primary.copy(alpha = 0.14f),
), ),
MentionVisualTransformation( MentionVisualTransformation(
mentionColor = palette.accentOrange, mentionColor = palette.accentOrange,
@ -452,7 +454,7 @@ fun MessageInput(
secs / 60, secs % 60, maxSecs / 60, maxSecs % 60 secs / 60, secs % 60, maxSecs / 60, maxSecs % 60
), ),
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.accentRed, color = colorScheme.error,
fontSize = (BASE_FONT_SIZE - 4).sp fontSize = (BASE_FONT_SIZE - 4).sp
) )
} }
@ -595,6 +597,7 @@ private fun SendButton(
enabled: Boolean = true enabled: Boolean = true
) { ) {
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState() val isPressed by interactionSource.collectIsPressedAsState()
@ -602,7 +605,7 @@ private fun SendButton(
isActive = enabled, isActive = enabled,
isPressed = isPressed, isPressed = isPressed,
// Private chats and channels keep their orange identity, disc and glyph together. // Private chats and channels keep their orange identity, disc and glyph together.
activeColor = if (isAccented) palette.accentOrange else palette.accentGreen, activeColor = if (isAccented) palette.accentOrange else colorScheme.primary,
modifier = modifier.clickable( modifier = modifier.clickable(
interactionSource = interactionSource, interactionSource = interactionSource,
indication = null, indication = null,
@ -624,13 +627,13 @@ fun CommandSuggestionsBox(
onSuggestionClick: (CommandSuggestion) -> Unit, onSuggestionClick: (CommandSuggestion) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
Column( Column(
modifier = modifier modifier = modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.background(palette.surface) .background(colorScheme.surface)
.border(1.dp, palette.outlineVariant, RoundedCornerShape(8.dp)) .border(1.dp, colorScheme.outlineVariant, RoundedCornerShape(8.dp))
.padding(vertical = 6.dp) .padding(vertical = 6.dp)
) { ) {
suggestions.forEach { suggestion: CommandSuggestion -> suggestions.forEach { suggestion: CommandSuggestion ->
@ -683,7 +686,7 @@ fun CommandSuggestionItem(
style = MaterialTheme.typography.bodySmall.copy( style = MaterialTheme.typography.bodySmall.copy(
fontFamily = BitchatFontFamily fontFamily = BitchatFontFamily
), ),
color = palette.textSecondary, color = colorScheme.onSurfaceVariant,
fontSize = (BASE_FONT_SIZE - 4).sp fontSize = (BASE_FONT_SIZE - 4).sp
) )
} }
@ -709,7 +712,7 @@ fun MentionSuggestionsBox(
onSuggestionClick: (String) -> Unit, onSuggestionClick: (String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
LazyColumn( LazyColumn(
modifier = modifier modifier = modifier
@ -721,8 +724,8 @@ fun MentionSuggestionsBox(
) )
) )
.clip(MentionSuggestionsShape) .clip(MentionSuggestionsShape)
.background(palette.surface) .background(colorScheme.surface)
.border(1.dp, palette.outlineVariant, MentionSuggestionsShape), .border(1.dp, colorScheme.outlineVariant, MentionSuggestionsShape),
contentPadding = PaddingValues(vertical = MentionSuggestionsVerticalPadding) contentPadding = PaddingValues(vertical = MentionSuggestionsVerticalPadding)
) { ) {
items( items(

View File

@ -43,11 +43,8 @@ fun LinkPreviewPill(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f val textColor = colorScheme.secondary
val backgroundColor = colorScheme.secondaryContainer.copy(alpha = 0.35f)
// iOS-style colors
val textColor = if (isDark) Color.Green else Color(red = 0f, green = 0.5f, blue = 0f)
val backgroundColor = if (isDark) Color.Gray.copy(alpha = 0.15f) else Color.Gray.copy(alpha = 0.08f)
val borderColor = textColor.copy(alpha = 0.3f) val borderColor = textColor.copy(alpha = 0.3f)
// Parse URL for host extraction // Parse URL for host extraction
@ -87,7 +84,7 @@ fun LinkPreviewPill(
Surface( Surface(
modifier = Modifier.size(60.dp), modifier = Modifier.size(60.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
color = Color.Blue.copy(alpha = 0.1f) color = colorScheme.secondaryContainer
) { ) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -97,7 +94,7 @@ fun LinkPreviewPill(
imageVector = Icons.Outlined.Link, imageVector = Icons.Outlined.Link,
contentDescription = stringResource(com.bitchat.android.R.string.cd_link), contentDescription = stringResource(com.bitchat.android.R.string.cd_link),
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = Color.Blue tint = colorScheme.secondary
) )
} }
} }

View File

@ -28,6 +28,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
@ -40,6 +42,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -71,7 +74,9 @@ import com.bitchat.android.ui.theme.BitchatMotion
import com.bitchat.android.ui.theme.LocalBitchatPalette import com.bitchat.android.ui.theme.LocalBitchatPalette
import com.bitchat.android.wifiaware.WifiAwareController import com.bitchat.android.wifiaware.WifiAwareController
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
/** /**
* Leading column width matching settings rows: 22.dp glyph + 16.dp gutter before title text. * Leading column width matching settings rows: 22.dp glyph + 16.dp gutter before title text.
@ -152,8 +157,8 @@ fun LocationChannelsSheet(
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val standardGreen = palette.accentGreen val standardGreen = colorScheme.primary
val standardBlue = palette.accentBlue val standardBlue = colorScheme.secondary
val nearbyChannels = remember(availableChannels) { val nearbyChannels = remember(availableChannels) {
availableChannels.filter { it.level != GeohashChannelLevel.BUILDING } availableChannels.filter { it.level != GeohashChannelLevel.BUILDING }
@ -185,7 +190,12 @@ fun LocationChannelsSheet(
Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), // Edge-to-edge + adjustResize reports the keyboard as WindowInsets.ime —
// without consuming it here the list stays full-height and the teleport
// field can't scroll above the keyboard.
modifier = Modifier
.fillMaxSize()
.imePadding(),
contentPadding = PaddingValues(top = 72.dp, bottom = 32.dp), contentPadding = PaddingValues(top = 72.dp, bottom = 32.dp),
verticalArrangement = Arrangement.spacedBy(0.dp) verticalArrangement = Arrangement.spacedBy(0.dp)
) { ) {
@ -203,7 +213,7 @@ fun LocationChannelsSheet(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding) .padding(horizontal = AboutHorizontalPadding)
.padding(top = 10.dp), .padding(top = 10.dp),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
ChannelOptionRow( ChannelOptionRow(
@ -253,7 +263,7 @@ fun LocationChannelsSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
ChannelOptionRow( ChannelOptionRow(
@ -292,7 +302,7 @@ fun LocationChannelsSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Column { Column {
@ -353,7 +363,7 @@ fun LocationChannelsSheet(
text = stringResource(R.string.location_permission_denied), text = stringResource(R.string.location_permission_denied),
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.accentRed color = colorScheme.error
) )
TextButton( TextButton(
onClick = { onClick = {
@ -389,7 +399,7 @@ fun LocationChannelsSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Column { Column {
@ -443,12 +453,7 @@ fun LocationChannelsSheet(
customError = null customError = null
}, },
onFocusGained = { onFocusGained = {
coroutineScope.launch { coroutineScope.launch { sheetState.expand() }
sheetState.expand()
listState.animateScrollToItem(
index = listState.layoutInfo.totalItemsCount - 1
)
}
}, },
onOpenMap = { onOpenMap = {
val normalized = customGeohash.trim().lowercase().replace("#", "") val normalized = customGeohash.trim().lowercase().replace("#", "")
@ -497,7 +502,7 @@ fun LocationChannelsSheet(
text = shownError, text = shownError,
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.accentRed, color = colorScheme.error,
modifier = Modifier.padding( modifier = Modifier.padding(
start = AboutHorizontalPadding + ChannelRowHorizontal, start = AboutHorizontalPadding + ChannelRowHorizontal,
top = 8.dp top = 8.dp
@ -518,7 +523,7 @@ fun LocationChannelsSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding), .padding(horizontal = AboutHorizontalPadding),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
ChannelSettingsToggleRow( ChannelSettingsToggleRow(
@ -638,7 +643,6 @@ private fun ChannelOptionRow(
onClick: () -> Unit onClick: () -> Unit
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
val (baseTitle, countSuffix) = splitTitleAndCount(title) val (baseTitle, countSuffix) = splitTitleAndCount(title)
Row( Row(
@ -657,7 +661,7 @@ private fun ChannelOptionRow(
Box( Box(
modifier = Modifier modifier = Modifier
.size(ChannelSelectedDot) .size(ChannelSelectedDot)
.background(palette.accentGreen, CircleShape) .background(colorScheme.primary, CircleShape)
) )
} }
leadingIcon != null -> { leadingIcon != null -> {
@ -686,7 +690,7 @@ private fun ChannelOptionRow(
fontSize = 14.sp, fontSize = 14.sp,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontWeight = if (titleBold) FontWeight.SemiBold else FontWeight.Medium, fontWeight = if (titleBold) FontWeight.SemiBold else FontWeight.Medium,
color = titleColor ?: palette.textPrimary color = titleColor ?: colorScheme.onSurface
) )
countSuffix?.let { count -> countSuffix?.let { count ->
AnimatedCountLabel( AnimatedCountLabel(
@ -694,7 +698,7 @@ private fun ChannelOptionRow(
text = count, text = count,
fontSize = 11.sp, fontSize = 11.sp,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
} }
} }
@ -703,7 +707,7 @@ private fun ChannelOptionRow(
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
lineHeight = 17.sp, lineHeight = 17.sp,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
} }
@ -719,7 +723,7 @@ private fun ChannelBookmarkButton(
bookmarked: Boolean, bookmarked: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
Box( Box(
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
@ -737,7 +741,7 @@ private fun ChannelBookmarkButton(
contentDescription = stringResource( contentDescription = stringResource(
if (bookmarked) R.string.cd_remove_bookmark else R.string.cd_add_bookmark if (bookmarked) R.string.cd_remove_bookmark else R.string.cd_add_bookmark
), ),
tint = if (bookmarked) palette.accentGreen else palette.textSecondary, tint = if (bookmarked) colorScheme.primary else colorScheme.onSurfaceVariant,
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} }
@ -745,7 +749,7 @@ private fun ChannelBookmarkButton(
@Composable @Composable
private fun ChannelLoadingRow() { private fun ChannelLoadingRow() {
val palette = LocalBitchatPalette.current val colorScheme = MaterialTheme.colorScheme
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -760,14 +764,14 @@ private fun ChannelLoadingRow() {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
strokeWidth = 2.dp, strokeWidth = 2.dp,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
} }
Text( Text(
text = stringResource(R.string.finding_nearby_channels), text = stringResource(R.string.finding_nearby_channels),
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.textSecondary color = colorScheme.onSurfaceVariant
) )
} }
} }
@ -785,6 +789,10 @@ private fun CustomGeohashRow(
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val density = LocalDensity.current
val imeInsets = WindowInsets.ime
val coroutineScope = rememberCoroutineScope()
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val normalized = customGeohash.trim().lowercase().replace("#", "") val normalized = customGeohash.trim().lowercase().replace("#", "")
val isValid = validateGeohash(normalized) val isValid = validateGeohash(normalized)
// Typing the last character of a valid geohash arms the button; cross-fading both the label // Typing the last character of a valid geohash arms the button; cross-fading both the label
@ -798,7 +806,7 @@ private fun CustomGeohashRow(
targetValue = if (isValid) { targetValue = if (isValid) {
colorScheme.primary.copy(alpha = 0.16f) colorScheme.primary.copy(alpha = 0.16f)
} else { } else {
palette.surfaceVariant colorScheme.surfaceVariant
}, },
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing), animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "teleportContainer" label = "teleportContainer"
@ -809,6 +817,7 @@ private fun CustomGeohashRow(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.bringIntoViewRequester(bringIntoViewRequester)
.padding(horizontal = ChannelRowHorizontal, vertical = ChannelRowVertical), .padding(horizontal = ChannelRowHorizontal, vertical = ChannelRowVertical),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -847,7 +856,22 @@ private fun CustomGeohashRow(
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.onFocusChanged { if (it.isFocused) onFocusGained() }, .onFocusChanged { focusState ->
if (!focusState.isFocused) return@onFocusChanged
onFocusGained()
// Wait until IME insets have landed so the LazyColumn has shrunk; bringIntoView
// against the full-height viewport leaves the field tucked under the keyboard.
// Re-request once the IME animation settles — early insets are still growing.
coroutineScope.launch {
withTimeoutOrNull(750) {
snapshotFlow { imeInsets.getBottom(density) }
.first { it > 0 }
}
bringIntoViewRequester.bringIntoView()
delay(BitchatMotion.STANDARD_MS.toLong())
bringIntoViewRequester.bringIntoView()
}
},
decorationBox = { inner -> decorationBox = { inner ->
if (customGeohash.isEmpty()) { if (customGeohash.isEmpty()) {
Text( Text(
@ -871,7 +895,7 @@ private fun CustomGeohashRow(
Icon( Icon(
imageVector = Icons.Filled.Map, imageVector = Icons.Filled.Map,
contentDescription = stringResource(R.string.cd_open_map), contentDescription = stringResource(R.string.cd_open_map),
tint = palette.textSecondary, tint = colorScheme.onSurfaceVariant,
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} }
@ -937,7 +961,7 @@ private fun ChannelSettingsToggleRow(
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = if (enabled) palette.textPrimary else palette.textTertiary color = if (enabled) colorScheme.onSurface else palette.textTertiary
) )
statusIndicator?.invoke() statusIndicator?.invoke()
} }
@ -946,7 +970,7 @@ private fun ChannelSettingsToggleRow(
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 17.sp, lineHeight = 17.sp,
color = if (enabled) palette.textSecondary else palette.textTertiary color = if (enabled) colorScheme.onSurfaceVariant else palette.textTertiary
) )
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@ -956,9 +980,9 @@ private fun ChannelSettingsToggleRow(
enabled = enabled, enabled = enabled,
colors = SwitchDefaults.colors( colors = SwitchDefaults.colors(
checkedThumbColor = Color.White, checkedThumbColor = Color.White,
checkedTrackColor = palette.accentGreen, checkedTrackColor = colorScheme.primary,
uncheckedThumbColor = Color.White, uncheckedThumbColor = Color.White,
uncheckedTrackColor = palette.surfaceVariant uncheckedTrackColor = colorScheme.surfaceVariant
) )
) )
} }

View File

@ -21,7 +21,6 @@ import com.bitchat.android.R
import com.bitchat.android.geohash.ChannelID import com.bitchat.android.geohash.ChannelID
import com.bitchat.android.geohash.LocationChannelManager import com.bitchat.android.geohash.LocationChannelManager
import com.bitchat.android.nostr.LocationNotesManager import com.bitchat.android.nostr.LocationNotesManager
import com.bitchat.android.ui.theme.LocalBitchatPalette
/** /**
* Location Notes button for MainHeader. * Location Notes button for MainHeader.
@ -35,7 +34,6 @@ fun LocationNotesButton(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
val context = LocalContext.current val context = LocalContext.current
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle() val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
@ -54,7 +52,7 @@ fun LocationNotesButton(
if (selectedLocationChannel is ChannelID.Mesh && locationEnabled) { if (selectedLocationChannel is ChannelID.Mesh && locationEnabled) {
val hasNotes = notesCount > 0 val hasNotes = notesCount > 0
val contentDescription = stringResource(R.string.cd_location_notes) val contentDescription = stringResource(R.string.cd_location_notes)
val normalTint = if (hasNotes) colorScheme.primary else palette.textSecondary val normalTint = if (hasNotes) colorScheme.primary else colorScheme.onSurfaceVariant
val torVisual = rememberTorConnectionVisual(normal = normalTint) val torVisual = rememberTorConnectionVisual(normal = normalTint)
Box( Box(

View File

@ -5,7 +5,6 @@ import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -51,10 +50,8 @@ fun LocationNotesSheet(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
val isDark = isSystemInDarkTheme() val colorScheme = MaterialTheme.colorScheme
val accentGreen = colorScheme.primary
// iOS color scheme
val accentGreen = if (isDark) Color.Green else Color(0xFF008000) // dark: green, light: dark green (0, 0.5, 0)
// Managers // Managers
val notesManager = remember { LocationNotesManager.getInstance() } val notesManager = remember { LocationNotesManager.getInstance() }
@ -468,7 +465,6 @@ private fun LocationNotesInputSection(
nickname: String?, nickname: String?,
onSend: () -> Unit onSend: () -> Unit
) { ) {
val isDark = isSystemInDarkTheme()
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
Column( Column(
@ -556,10 +552,8 @@ private fun LocationNotesInputSection(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
tint = if (!sendButtonEnabled) { tint = if (!sendButtonEnabled) {
colorScheme.onSurface.copy(alpha = 0.5f) colorScheme.onSurface.copy(alpha = 0.5f)
} else if (isDark) {
Color.Black // Black arrow on green in dark theme
} else { } else {
Color.White // White arrow on green in light theme colorScheme.onPrimary
} }
) )
} }

View File

@ -47,6 +47,7 @@ import com.bitchat.android.identity.SecureIdentityStateManager
import com.bitchat.android.ui.theme.BASE_FONT_SIZE import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import com.bitchat.android.ui.theme.BitchatMotion import com.bitchat.android.ui.theme.BitchatMotion
import com.bitchat.android.ui.theme.LocalBitchatPalette import com.bitchat.android.ui.theme.LocalBitchatPalette
import com.bitchat.android.ui.theme.colorForPeerSeed
import com.bitchat.android.nostr.GeohashAliasRegistry import com.bitchat.android.nostr.GeohashAliasRegistry
import com.bitchat.android.nostr.GeohashConversationRegistry import com.bitchat.android.nostr.GeohashConversationRegistry
import com.bitchat.android.services.ContactDirectory import com.bitchat.android.services.ContactDirectory
@ -132,7 +133,7 @@ fun MeshPeerListSheet(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding) .padding(horizontal = AboutHorizontalPadding)
.padding(top = 10.dp), .padding(top = 10.dp),
color = LocalBitchatPalette.current.surface, color = MaterialTheme.colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Column { Column {
@ -222,7 +223,7 @@ fun MeshPeerListSheet(
Icon( Icon(
imageVector = Icons.Outlined.QrCode, imageVector = Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.verify_title), contentDescription = stringResource(R.string.verify_title),
tint = LocalBitchatPalette.current.textSecondary, tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} }
@ -265,7 +266,7 @@ private fun ChannelRow(
Box( Box(
modifier = Modifier modifier = Modifier
.size(SheetRowSelectedDot) .size(SheetRowSelectedDot)
.background(palette.accentGreen, CircleShape) .background(colorScheme.primary, CircleShape)
) )
} else if (unreadCount > 0) { } else if (unreadCount > 0) {
UnreadBadge(count = unreadCount, colorScheme = colorScheme) UnreadBadge(count = unreadCount, colorScheme = colorScheme)
@ -286,7 +287,7 @@ private fun ChannelRow(
text = channel, text = channel,
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
fontSize = 14.sp, fontSize = 14.sp,
color = if (isSelected) colorScheme.primary else palette.textPrimary, color = if (isSelected) colorScheme.primary else colorScheme.onSurface,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
maxLines = 1, maxLines = 1,
@ -331,7 +332,7 @@ fun PeopleSection(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AboutHorizontalPadding) .padding(horizontal = AboutHorizontalPadding)
.padding(top = 10.dp), .padding(top = 10.dp),
color = palette.surface, color = colorScheme.surface,
shape = AboutCardShape shape = AboutCardShape
) { ) {
Column { Column {
@ -588,7 +589,10 @@ private fun PeerItem(
// Get consistent peer color (iOS-compatible) // Get consistent peer color (iOS-compatible)
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val assignedColor = viewModel.colorForMeshPeer(peerID, palette.isDark) val assignedColor = colorForPeerSeed(
viewModel.peerColorSeedForMeshPeer(peerID),
palette
)
val baseColor = if (isMe) palette.accentOrange else assignedColor val baseColor = if (isMe) palette.accentOrange else assignedColor
Row( Row(
@ -606,7 +610,7 @@ private fun PeerItem(
Box( Box(
modifier = Modifier modifier = Modifier
.size(SheetRowSelectedDot) .size(SheetRowSelectedDot)
.background(palette.accentGreen, CircleShape) .background(colorScheme.primary, CircleShape)
) )
} else if (hasUnreadDM) { } else if (hasUnreadDM) {
Icon( Icon(
@ -642,7 +646,7 @@ private fun PeerItem(
else -> "Routed" else -> "Routed"
}, },
modifier = Modifier.size(PeerRowIconSize), modifier = Modifier.size(PeerRowIconSize),
tint = palette.textSecondary tint = colorScheme.onSurfaceVariant
) )
} }
} }
@ -679,7 +683,7 @@ private fun PeerItem(
imageVector = Icons.Filled.Verified, imageVector = Icons.Filled.Verified,
contentDescription = stringResource(R.string.verify_title), contentDescription = stringResource(R.string.verify_title),
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = palette.accentGreen tint = colorScheme.primary
) )
} }
} }
@ -866,7 +870,6 @@ fun PrivateChatSheet(
val sheetState = rememberModalBottomSheetState( val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true skipPartiallyExpanded = true
) )
val palette = LocalBitchatPalette.current
if (isPresented) { if (isPresented) {
BitchatBottomSheet( BitchatBottomSheet(
@ -879,7 +882,7 @@ fun PrivateChatSheet(
) { ) {
Spacer(modifier = Modifier.height(64.dp)) Spacer(modifier = Modifier.height(64.dp))
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant) HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
// Messages list // Messages list
var forceScrollToBottom by remember { mutableStateOf(false) } var forceScrollToBottom by remember { mutableStateOf(false) }

View File

@ -498,6 +498,7 @@ fun MessageItem(
currentUserNickname = currentUserNickname, currentUserNickname = currentUserNickname,
myPeerID = meshService.myPeerID, myPeerID = meshService.myPeerID,
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeSender = showSender includeSender = showSender
) )
@ -516,7 +517,7 @@ fun MessageItem(
}, },
onLongPress = { onMessageLongPress?.invoke(message) }, onLongPress = { onMessageLongPress?.invoke(message) },
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.textPrimary, color = colorScheme.onSurface,
) )
// Try to load the file packet from the path // Try to load the file packet from the path
@ -564,14 +565,14 @@ fun MessageItem(
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(4.dp) .padding(4.dp)
.size(22.dp) .size(22.dp)
.background(palette.surfaceVariant.copy(alpha = 0.85f), CircleShape) .background(colorScheme.surfaceVariant.copy(alpha = 0.85f), CircleShape)
.clickable { onCancelTransfer?.invoke(message) }, .clickable { onCancelTransfer?.invoke(message) },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Close, imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.cd_cancel), contentDescription = stringResource(R.string.cd_cancel),
tint = palette.textPrimary, tint = colorScheme.onSurface,
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
} }
@ -591,10 +592,10 @@ fun MessageItem(
if (message.sender == "system") { if (message.sender == "system") {
// Background narration: `// Tor started. Routing all chats…` // Background narration: `// Tor started. Routing all chats…`
val annotatedText = remember(message, palette) { val annotatedText = remember(message, colorScheme.onSurface) {
formatSystemMessage( formatSystemMessage(
message = message, message = message,
palette = palette, contentColor = colorScheme.onSurface,
timeFormatter = timeFormatter timeFormatter = timeFormatter
) )
} }
@ -614,7 +615,7 @@ fun MessageItem(
softWrap = true, softWrap = true,
overflow = TextOverflow.Visible, overflow = TextOverflow.Visible,
style = ChatVisualTokens.SystemActionStyle.copy( style = ChatVisualTokens.SystemActionStyle.copy(
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha), color = colorScheme.onSurface.copy(alpha = ChatVisualTokens.MutedTextAlpha),
) )
) )
} else { } else {
@ -660,11 +661,20 @@ internal fun TextMessageLayout(
} }
// The timestamp trails the body rather than occupying its own column, so a short message // The timestamp trails the body rather than occupying its own column, so a short message
// no longer reserves a full-width row for eight grey characters. // no longer reserves a full-width row for eight grey characters.
val bodyText = remember(displayMessage, currentUserNickname, palette, timeFormatter) { val bodyText = remember(
displayMessage,
currentUserNickname,
palette,
colorScheme.onSurface,
colorScheme.secondary,
timeFormatter
) {
formatTextMessageBody( formatTextMessageBody(
message = displayMessage, message = displayMessage,
currentUserNickname = currentUserNickname, currentUserNickname = currentUserNickname,
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
) )
} }
@ -728,7 +738,7 @@ internal fun TextMessageLayout(
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
softWrap = true, softWrap = true,
overflow = TextOverflow.Visible, overflow = TextOverflow.Visible,
style = MessageBodyTextStyle.copy(color = palette.textPrimary), style = MessageBodyTextStyle.copy(color = colorScheme.onSurface),
) )
} }
} }
@ -736,7 +746,6 @@ internal fun TextMessageLayout(
@Composable @Composable
fun DeliveryStatusIcon(status: DeliveryStatus) { fun DeliveryStatusIcon(status: DeliveryStatus) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
// Status advances on its own as acks come back, so a hard glyph swap reads as a flicker. // Status advances on its own as acks come back, so a hard glyph swap reads as a flicker.
// Keyed on the status *type* rather than the instance, because Delivered/Read carry a // Keyed on the status *type* rather than the instance, because Delivered/Read carry a
@ -758,9 +767,9 @@ fun DeliveryStatusIcon(status: DeliveryStatus) {
DeliveryStatus.Delivered::class -> DeliveryStatus.Delivered::class ->
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.8f), FontWeight.Normal) Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.8f), FontWeight.Normal)
DeliveryStatus.Read::class -> DeliveryStatus.Read::class ->
Triple(R.string.status_delivered, palette.accentBlue, FontWeight.Bold) Triple(R.string.status_delivered, colorScheme.secondary, FontWeight.Bold)
DeliveryStatus.Failed::class -> DeliveryStatus.Failed::class ->
Triple(R.string.status_failed, palette.accentRed, FontWeight.Normal) Triple(R.string.status_failed, colorScheme.error, FontWeight.Normal)
// A single subdued check, without the numeric label. // A single subdued check, without the numeric label.
else -> else ->
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal) Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal)

View File

@ -0,0 +1,33 @@
package com.bitchat.android.ui
import com.bitchat.android.model.BitchatMessage
import java.util.Locale
/**
* Stable, presentation-neutral identity used to derive a peer hue.
*
* ViewModels may expose this value, but only the UI theme resolves it to a rendered color.
*/
@JvmInline
value class PeerColorSeed(val value: String)
fun meshPeerColorSeed(peerID: String): PeerColorSeed =
PeerColorSeed("noise:${peerID.lowercase(Locale.ROOT)}")
fun nostrPeerColorSeed(pubkeyHex: String): PeerColorSeed =
PeerColorSeed("nostr:${pubkeyHex.lowercase(Locale.ROOT)}")
fun peerColorSeedForMessage(message: BitchatMessage): PeerColorSeed {
val value = when {
message.senderPeerID?.startsWith("nostr:") == true ||
message.senderPeerID?.startsWith("nostr_") == true -> {
"nostr:${message.senderPeerID.lowercase(Locale.ROOT)}"
}
message.senderPeerID?.length == 16 || message.senderPeerID?.length == 64 -> {
"noise:${message.senderPeerID.lowercase(Locale.ROOT)}"
}
else -> message.sender.lowercase(Locale.ROOT)
}
return PeerColorSeed(value)
}

View File

@ -10,7 +10,6 @@ import androidx.compose.material.icons.outlined.Warning as OutlinedWarning
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -70,9 +69,9 @@ fun SecurityVerificationSheet(
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle() val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle() val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
val isDark = isSystemInDarkTheme() val colorScheme = MaterialTheme.colorScheme
val accent = if (isDark) Color.Green else Color(0xFF008000) val accent = colorScheme.primary
val boxColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.06f) val boxColor = colorScheme.surfaceVariant
val peerHexRegex = remember { Regex("^[0-9a-fA-F]{16}$") } val peerHexRegex = remember { Regex("^[0-9a-fA-F]{16}$") }
BitchatBottomSheet( BitchatBottomSheet(

View File

@ -19,7 +19,6 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -99,8 +98,7 @@ fun VerificationSheet(
) { ) {
if (!isPresented) return if (!isPresented) return
val isDark = isSystemInDarkTheme() val accent = MaterialTheme.colorScheme.primary
val accent = if (isDark) Color.Green else Color(0xFF008000)
var selectedTab by remember { mutableStateOf(0) } // 0 = My Code, 1 = Scan var selectedTab by remember { mutableStateOf(0) } // 0 = My Code, 1 = Scan
val nickname by viewModel.nickname.collectAsStateWithLifecycle() val nickname by viewModel.nickname.collectAsStateWithLifecycle()

View File

@ -55,6 +55,7 @@ fun AudioMessageItem(
currentUserNickname = currentUserNickname, currentUserNickname = currentUserNickname,
myPeerID = meshService.myPeerID, myPeerID = meshService.myPeerID,
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeSender = showSender includeSender = showSender
) )
@ -73,7 +74,7 @@ fun AudioMessageItem(
}, },
onLongPress = { onMessageLongPress?.invoke(message) }, onLongPress = { onMessageLongPress?.invoke(message) },
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.textPrimary, color = colorScheme.onSurface,
) )
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {

View File

@ -53,6 +53,7 @@ fun ImageMessageItem(
currentUserNickname = currentUserNickname, currentUserNickname = currentUserNickname,
myPeerID = meshService.myPeerID, myPeerID = meshService.myPeerID,
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeSender = showSender includeSender = showSender
) )
@ -71,7 +72,7 @@ fun ImageMessageItem(
}, },
onLongPress = { onMessageLongPress?.invoke(message) }, onLongPress = { onMessageLongPress?.invoke(message) },
fontFamily = BitchatFontFamily, fontFamily = BitchatFontFamily,
color = palette.textPrimary, color = colorScheme.onSurface,
) )
val context = LocalContext.current val context = LocalContext.current

View File

@ -5,40 +5,18 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
/** /**
* Semantic design tokens for the bitchat redesign. * Bitchat-specific color tokens that do not have a faithful Material 3 semantic role.
* *
* Material3's [androidx.compose.material3.ColorScheme] cannot express what this app needs: * Standard backgrounds, surfaces, text, outlines, primary/secondary accents, and errors belong
* `onSurface` is the terminal green used for chrome, while message bodies must render in a * to [androidx.compose.material3.MaterialTheme.colorScheme]. Keeping only the extra app semantics
* neutral near-white. Rather than repurposing M3 slots (which would repaint ~40 unrelated * here lets Material components inherit correct defaults without losing Bitchat's identity.
* files unpredictably), the redesign exposes an explicit palette through
* [LocalBitchatPalette].
*
* Consumers should prefer these tokens over ad-hoc hex literals so accents stay consistent
* and light/dark pairs are defined in exactly one place.
*/ */
@Immutable @Immutable
data class BitchatPalette( data class BitchatPalette(
/** Authoritative dark-mode flag. Prefer this over summing background channels. */
val isDark: Boolean,
// MARK: - Surfaces
/** Screen, header and composer background. True black in dark mode. */
val background: Color,
/** Sheets, cards and list rows: one step above [background]. */
val surface: Color,
/** Composer pill fill, chips, selected rows: one step above [surface]. */
val surfaceVariant: Color,
/** Borders, drawn at full opacity (no `.copy(alpha = ...)` needed). */
val outline: Color,
/** Hairline dividers. */
val outlineVariant: Color,
// MARK: - Form controls // MARK: - Form controls
/** /**
* Resting border for text inputs. Deliberately a neutral grey rather than the green-tinted * Resting border for text inputs. Deliberately a neutral grey rather than the green-tinted
* [outline]: the composer is the one surface the user stares at while typing, and a green * Material outline: the composer is the one surface the user stares at while typing.
* cast there made the whole bottom of the screen read as "terminal chrome" instead of a
* place to write.
*/ */
val inputOutline: Color, val inputOutline: Color,
/** Border for a focused text input. A step brighter, still neutral. */ /** Border for a focused text input. A step brighter, still neutral. */
@ -54,72 +32,45 @@ data class BitchatPalette(
/** Resting disc behind the composer's action glyphs. Neutral grey. */ /** Resting disc behind the composer's action glyphs. Neutral grey. */
val inputButton: Color, val inputButton: Color,
// MARK: - Text // MARK: - Extra semantics
/** Message bodies and row titles. Neutral, not green. */
val textPrimary: Color,
/** Subtitles, secondary metadata, system/action messages. */
val textSecondary: Color,
/** Timestamps, placeholders, section labels, disabled states. */ /** Timestamps, placeholders, section labels, disabled states. */
val textTertiary: Color, val textTertiary: Color,
// MARK: - Accents
/** Geohash channels, bookmark-on, connected/OK states. */
val accentGreen: Color,
/** Mesh channel, links, geohash references. */
val accentBlue: Color,
/** Self, mentions targeting you, unread DMs. */ /** Self, mentions targeting you, unread DMs. */
val accentOrange: Color, val accentOrange: Color,
/** Errors, warning card, destructive actions. */
val accentRed: Color,
/** Nostr reachability. */ /** Nostr reachability. */
val accentPurple: Color, 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,
) )
val DarkBitchatPalette = BitchatPalette( val DarkBitchatPalette = BitchatPalette(
isDark = true,
// True black. OLED panels switch these pixels off entirely, which is both the terminal
// aesthetic the app is going for and a real battery win on a screen that is mostly
// background.
background = Color(0xFF000000),
surface = Color(0xFF0E150E),
surfaceVariant = Color(0xFF182118),
outline = Color(0xFF2A3A2A),
outlineVariant = Color(0xFF1C271C),
inputOutline = Color(0xFF333635), inputOutline = Color(0xFF333635),
inputOutlineFocused = Color(0xFF5A605D), inputOutlineFocused = Color(0xFF5A605D),
inputSurface = Color(0xFF0B0B0B), inputSurface = Color(0xFF0B0B0B),
inputSurfaceFocused = Color(0xFF151515), inputSurfaceFocused = Color(0xFF151515),
inputButton = Color(0xFF1E1E1E), inputButton = Color(0xFF1E1E1E),
textPrimary = Color(0xFFF5F5F5),
textSecondary = Color(0xFF9AA69A),
textTertiary = Color(0xFF6B776B), textTertiary = Color(0xFF6B776B),
accentGreen = Color(0xFF32D74B),
accentBlue = Color(0xFF0A84FF),
accentOrange = Color(0xFFFF9F0A), accentOrange = Color(0xFFFF9F0A),
accentRed = Color(0xFFFF453A),
accentPurple = Color(0xFFBF5AF2), accentPurple = Color(0xFFBF5AF2),
peerColorSaturation = 1f,
peerColorValue = 1f,
) )
val LightBitchatPalette = BitchatPalette( val LightBitchatPalette = BitchatPalette(
isDark = false,
background = Color(0xFFFFFFFF),
surface = Color(0xFFF2F6F2),
surfaceVariant = Color(0xFFE7EDE7),
outline = Color(0xFFCBD6CB),
outlineVariant = Color(0xFFDEE6DE),
inputOutline = Color(0xFFCFD3D1), inputOutline = Color(0xFFCFD3D1),
inputOutlineFocused = Color(0xFF8E9490), inputOutlineFocused = Color(0xFF8E9490),
inputSurface = Color(0xFFFAFAFA), inputSurface = Color(0xFFFAFAFA),
inputSurfaceFocused = Color(0xFFF2F2F2), inputSurfaceFocused = Color(0xFFF2F2F2),
inputButton = Color(0xFFE8E8E8), inputButton = Color(0xFFE8E8E8),
textPrimary = Color(0xFF131A13),
textSecondary = Color(0xFF4C574C),
textTertiary = Color(0xFF757F75), textTertiary = Color(0xFF757F75),
accentGreen = Color(0xFF248A3D),
accentBlue = Color(0xFF007AFF),
accentOrange = Color(0xFFFF9500), accentOrange = Color(0xFFFF9500),
accentRed = Color(0xFFD70015),
accentPurple = Color(0xFFAF52DE), accentPurple = Color(0xFFAF52DE),
peerColorSaturation = 0.85f,
peerColorValue = 0.45f,
) )
val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette } val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette }

View File

@ -0,0 +1,30 @@
package com.bitchat.android.ui.theme
import androidx.compose.ui.graphics.Color
import com.bitchat.android.ui.PeerColorSeed
import kotlin.math.abs
/**
* Resolve a peer's stable seed using the active Bitchat theme's explicit chroma tokens.
*
* The djb2 hash and hue adjustment are byte-identical to the iOS implementation. Orange is
* avoided because it is reserved for the current user.
*/
fun colorForPeerSeed(seed: PeerColorSeed, palette: BitchatPalette): Color {
var hash = 5381UL
for (byte in seed.value.toByteArray()) {
hash = ((hash shl 5) + hash) + byte.toUByte().toULong()
}
var hue = (hash % 360UL).toDouble() / 360.0
val orange = 30.0 / 360.0
if (abs(hue - orange) < 0.05) {
hue = (hue + 0.12) % 1.0
}
return Color.hsv(
hue = (hue * 360).toFloat(),
saturation = palette.peerColorSaturation,
value = palette.peerColorValue
)
}

View File

@ -17,40 +17,51 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
// Colors that match the iOS bitchat theme. // Standard UI semantics live in Material so stock components and custom Bitchat composables
// Surface/outline/error slots are derived from BitchatPalette so that screens which have not // share one source of truth. LocalBitchatPalette below only supplies app-specific extra colors.
// yet migrated to the explicit palette still pick up the redesigned tinted surfaces. internal val DarkBitchatColorScheme = darkColorScheme(
private val DarkColorScheme = darkColorScheme( primary = Color(0xFF32D74B),
primary = Color(0xFF39FF14), // Bright green (terminal-like)
onPrimary = Color.Black, onPrimary = Color.Black,
secondary = Color(0xFF2ECB10), // Darker green primaryContainer = Color(0xFF163D1D),
onPrimaryContainer = Color(0xFFB8F5C1),
secondary = Color(0xFF0A84FF),
onSecondary = Color.Black, onSecondary = Color.Black,
background = DarkBitchatPalette.background, secondaryContainer = Color(0xFF082E54),
onBackground = Color(0xFF39FF14), // Green on near-black onSecondaryContainer = Color(0xFFC2E0FF),
surface = DarkBitchatPalette.surface, tertiary = DarkBitchatPalette.accentOrange,
onSurface = Color(0xFF39FF14), // Green text onTertiary = Color.Black,
surfaceVariant = DarkBitchatPalette.surfaceVariant, background = Color(0xFF000000),
onSurfaceVariant = DarkBitchatPalette.textSecondary, onBackground = Color(0xFFF5F5F5),
outline = DarkBitchatPalette.outline, surface = Color(0xFF0E150E),
outlineVariant = DarkBitchatPalette.outlineVariant, onSurface = Color(0xFFF5F5F5),
error = DarkBitchatPalette.accentRed, surfaceVariant = Color(0xFF182118),
onSurfaceVariant = Color(0xFF9AA69A),
outline = Color(0xFF2A3A2A),
outlineVariant = Color(0xFF1C271C),
error = Color(0xFFFF453A),
onError = Color.Black onError = Color.Black
) )
private val LightColorScheme = lightColorScheme( internal val LightBitchatColorScheme = lightColorScheme(
primary = Color(0xFF008000), // Dark green primary = Color(0xFF248A3D),
onPrimary = Color.White, onPrimary = Color.White,
secondary = Color(0xFF006600), // Even darker green primaryContainer = Color(0xFFD5F1D8),
onPrimaryContainer = Color(0xFF0A3212),
secondary = Color(0xFF007AFF),
onSecondary = Color.White, onSecondary = Color.White,
background = LightBitchatPalette.background, secondaryContainer = Color(0xFFD6E9FF),
onBackground = Color(0xFF008000), // Dark green on off-white onSecondaryContainer = Color(0xFF002C5C),
surface = LightBitchatPalette.surface, tertiary = LightBitchatPalette.accentOrange,
onSurface = Color(0xFF008000), // Dark green text onTertiary = Color.Black,
surfaceVariant = LightBitchatPalette.surfaceVariant, background = Color(0xFFFFFFFF),
onSurfaceVariant = LightBitchatPalette.textSecondary, onBackground = Color(0xFF131A13),
outline = LightBitchatPalette.outline, surface = Color(0xFFF2F6F2),
outlineVariant = LightBitchatPalette.outlineVariant, onSurface = Color(0xFF131A13),
error = LightBitchatPalette.accentRed, surfaceVariant = Color(0xFFE7EDE7),
onSurfaceVariant = Color(0xFF4C574C),
outline = Color(0xFFCBD6CB),
outlineVariant = Color(0xFFDEE6DE),
error = Color(0xFFD70015),
onError = Color.White onError = Color.White
) )
@ -71,7 +82,7 @@ fun BitchatTheme(
} }
} }
val colorScheme = if (shouldUseDark) DarkColorScheme else LightColorScheme val colorScheme = if (shouldUseDark) DarkBitchatColorScheme else LightBitchatColorScheme
val palette = if (shouldUseDark) DarkBitchatPalette else LightBitchatPalette val palette = if (shouldUseDark) DarkBitchatPalette else LightBitchatPalette
val view = LocalView.current val view = LocalView.current

View File

@ -7,16 +7,18 @@ import androidx.compose.ui.unit.sp
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.ui.theme.BitchatFontFamily import com.bitchat.android.ui.theme.BitchatFontFamily
import com.bitchat.android.ui.theme.ChatVisualTokens import com.bitchat.android.ui.theme.ChatVisualTokens
import com.bitchat.android.ui.theme.DarkBitchatColorScheme
import com.bitchat.android.ui.theme.DarkBitchatPalette import com.bitchat.android.ui.theme.DarkBitchatPalette
import com.bitchat.android.ui.theme.LightBitchatColorScheme
import com.bitchat.android.ui.theme.LightBitchatPalette import com.bitchat.android.ui.theme.LightBitchatPalette
import com.bitchat.android.ui.theme.MessageBodyTextStyle import com.bitchat.android.ui.theme.MessageBodyTextStyle
import com.bitchat.android.ui.theme.MessageSenderTextStyle import com.bitchat.android.ui.theme.MessageSenderTextStyle
import com.bitchat.android.ui.theme.colorForPeerSeed
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -33,6 +35,7 @@ class ChatUIUtilsTest {
} }
private val palette = DarkBitchatPalette private val palette = DarkBitchatPalette
private val colorScheme = DarkBitchatColorScheme
private fun message( private fun message(
content: String, content: String,
@ -71,6 +74,8 @@ class ChatUIUtilsTest {
message = message("hello there"), message = message("hello there"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
) )
@ -89,6 +94,8 @@ class ChatUIUtilsTest {
message = message("mined", powDifficulty = 8), message = message("mined", powDifficulty = 8),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
) )
@ -106,6 +113,8 @@ class ChatUIUtilsTest {
message = message("hello there"), message = message("hello there"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeTimestamp = false, includeTimestamp = false,
) )
@ -119,12 +128,14 @@ class ChatUIUtilsTest {
message = message("plain words"), message = message("plain words"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeTimestamp = false, includeTimestamp = false,
) )
val textStyle = body.spanStyles.first { it.start == 0 } val textStyle = body.spanStyles.first { it.start == 0 }
assertEquals(palette.textPrimary, textStyle.item.color) assertEquals(colorScheme.onSurface, textStyle.item.color)
} }
@Test @Test
@ -146,6 +157,8 @@ class ChatUIUtilsTest {
message = message("my own words", sender = "bob"), message = message("my own words", sender = "bob"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeTimestamp = false, includeTimestamp = false,
) )
@ -170,6 +183,8 @@ class ChatUIUtilsTest {
message = message("hey @carol#04af what's up"), message = message("hey @carol#04af what's up"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeTimestamp = false, includeTimestamp = false,
) )
@ -188,6 +203,8 @@ class ChatUIUtilsTest {
message = message("ping @bob now"), message = message("ping @bob now"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeTimestamp = false, includeTimestamp = false,
) )
@ -208,11 +225,13 @@ class ChatUIUtilsTest {
message = message("cc @carol#04af"), message = message("cc @carol#04af"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeTimestamp = false, includeTimestamp = false,
) )
val expected = colorForPeerSeed("carol#04af", palette.isDark) val expected = colorForPeerSeed(PeerColorSeed("carol#04af"), palette)
val chip = mentionChipSpans(body).single() val chip = mentionChipSpans(body).single()
assertEquals(expected.copy(alpha = MENTION_CHIP_ALPHA), chip.item.background) assertEquals(expected.copy(alpha = MENTION_CHIP_ALPHA), chip.item.background)
} }
@ -223,6 +242,8 @@ class ChatUIUtilsTest {
message = message("no mentions in here"), message = message("no mentions in here"),
currentUserNickname = "bob", currentUserNickname = "bob",
palette = palette, palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
includeTimestamp = false, includeTimestamp = false,
) )
@ -236,7 +257,7 @@ class ChatUIUtilsTest {
fun `system message uses a double-slash prefix and no brackets`() { fun `system message uses a double-slash prefix and no brackets`() {
val text = formatSystemMessage( val text = formatSystemMessage(
message = message("Tor started. Routing all chats via Tor", sender = "system"), message = message("Tor started. Routing all chats via Tor", sender = "system"),
palette = palette, contentColor = colorScheme.onSurface,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
).text ).text
@ -249,7 +270,7 @@ class ChatUIUtilsTest {
// messages despite being lower-priority narration. // messages despite being lower-priority narration.
val annotated = formatSystemMessage( val annotated = formatSystemMessage(
message = message("tor restarting", sender = "system"), message = message("tor restarting", sender = "system"),
palette = palette, contentColor = colorScheme.onSurface,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
) )
@ -260,7 +281,7 @@ class ChatUIUtilsTest {
fun `system action and timestamp use their exported weights sizes and opacity`() { fun `system action and timestamp use their exported weights sizes and opacity`() {
val annotated = formatSystemMessage( val annotated = formatSystemMessage(
message = message("tor restarting", sender = "system"), message = message("tor restarting", sender = "system"),
palette = palette, contentColor = colorScheme.onSurface,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
) )
@ -273,10 +294,10 @@ class ChatUIUtilsTest {
assertEquals(12.sp, action.fontSize) assertEquals(12.sp, action.fontSize)
assertEquals(FontWeight.Medium, action.fontWeight) assertEquals(FontWeight.Medium, action.fontWeight)
assertEquals(palette.textPrimary.copy(alpha = 0.5f), action.color) assertEquals(colorScheme.onSurface.copy(alpha = 0.5f), action.color)
assertEquals(10.sp, time.fontSize) assertEquals(10.sp, time.fontSize)
assertEquals(FontWeight.Normal, time.fontWeight) assertEquals(FontWeight.Normal, time.fontWeight)
assertEquals(palette.textPrimary.copy(alpha = 0.5f), time.color) assertEquals(colorScheme.onSurface.copy(alpha = 0.5f), time.color)
} }
// MARK: - Sender label // MARK: - Sender label
@ -327,12 +348,25 @@ class ChatUIUtilsTest {
// MARK: - Peer colors // MARK: - Peer colors
@Test
fun `peer seed factories normalize identities without resolving UI colors`() {
assertEquals(
PeerColorSeed("noise:abcdef"),
meshPeerColorSeed("ABCDEF")
)
assertEquals(
PeerColorSeed("nostr:abcdef"),
nostrPeerColorSeed("ABCDEF")
)
}
@Test @Test
fun `peer color hue is stable across light and dark, only chroma differs`() { 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 // Hue derivation must stay byte-identical to iOS; only saturation/value are tuned for
// the redesigned neutral message body. // the redesigned neutral message body.
val dark = colorForPeerSeed("noise:abc", isDark = true) val seed = PeerColorSeed("noise:abc")
val light = colorForPeerSeed("noise:abc", isDark = false) val dark = colorForPeerSeed(seed, DarkBitchatPalette)
val light = colorForPeerSeed(seed, LightBitchatPalette)
val darkHsv = FloatArray(3) val darkHsv = FloatArray(3)
val lightHsv = FloatArray(3) val lightHsv = FloatArray(3)
@ -342,13 +376,18 @@ class ChatUIUtilsTest {
assertEquals(darkHsv[0].toDouble(), lightHsv[0].toDouble(), 1.0) assertEquals(darkHsv[0].toDouble(), lightHsv[0].toDouble(), 1.0)
assertEquals(1.0, darkHsv[1].toDouble(), 0.01) assertEquals(1.0, darkHsv[1].toDouble(), 0.01)
assertEquals(1.0, darkHsv[2].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)
} }
@Test @Test
fun `peer color avoids the orange hue reserved for self`() { fun `peer color avoids the orange hue reserved for self`() {
// Sweep a range of seeds; none may land within the reserved orange band. // Sweep a range of seeds; none may land within the reserved orange band.
repeat(500) { i -> repeat(500) { i ->
val color = colorForPeerSeed("noise:seed$i", isDark = true) val color = colorForPeerSeed(
PeerColorSeed("noise:seed$i"),
DarkBitchatPalette
)
val hsv = FloatArray(3) val hsv = FloatArray(3)
rgbToHsv(color.red, color.green, color.blue, hsv) rgbToHsv(color.red, color.green, color.blue, hsv)
val distanceFromOrange = kotlin.math.abs(hsv[0] - 30f) val distanceFromOrange = kotlin.math.abs(hsv[0] - 30f)
@ -360,11 +399,12 @@ class ChatUIUtilsTest {
} }
@Test @Test
fun `light palette is not the dark palette`() { fun `material owns standard text while Bitchat palette owns peer chroma`() {
assertNull(null) assertEquals(Color(0xFFF5F5F5), DarkBitchatColorScheme.onSurface)
assertEquals(Color(0xFFF5F5F5), DarkBitchatPalette.textPrimary) assertTrue(LightBitchatColorScheme.onSurface != DarkBitchatColorScheme.onSurface)
assertTrue(LightBitchatPalette.textPrimary != DarkBitchatPalette.textPrimary) assertTrue(
assertTrue(LightBitchatPalette.isDark != DarkBitchatPalette.isDark) LightBitchatPalette.peerColorValue != DarkBitchatPalette.peerColorValue
)
} }
private fun rgbToHsv(r: Float, g: Float, b: Float, out: FloatArray) { private fun rgbToHsv(r: Float, g: Float, b: Float, out: FloatArray) {