mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-15 06:56:30 +00:00
colors
This commit is contained in:
parent
c1c7704c32
commit
25e9779f73
@ -6,26 +6,26 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
|
||||
@Composable
|
||||
fun CloseButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
// 44.dp to match every other tap target in the app's chrome.
|
||||
modifier = modifier.size(44.dp),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = palette.accentGreen,
|
||||
contentColor = colorScheme.primary,
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
) {
|
||||
|
||||
@ -120,7 +120,6 @@ internal fun SheetIconSectionHeader(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
@ -152,7 +151,7 @@ internal fun SheetIconSectionHeader(
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 17.sp,
|
||||
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). */
|
||||
@Composable
|
||||
internal fun SheetCardDivider() {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = SheetRowDividerInset),
|
||||
thickness = 1.dp,
|
||||
color = palette.outlineVariant
|
||||
color = colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
|
||||
@ -212,7 +211,7 @@ internal fun AboutHero(
|
||||
text = stringResource(R.string.about_tagline),
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 16.sp,
|
||||
color = palette.textSecondary
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
@ -238,7 +237,6 @@ internal fun AboutTabBar(
|
||||
onSelect: (AboutTab) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val density = LocalDensity.current
|
||||
|
||||
@ -274,7 +272,7 @@ internal fun AboutTabBar(
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
||||
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
// Lambda overload: the offset is animated every frame, and the non-lambda
|
||||
@ -295,11 +293,10 @@ private fun AboutTabLabel(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
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),
|
||||
label = "aboutTabLabelColor"
|
||||
)
|
||||
@ -328,7 +325,6 @@ private fun AboutInstructionRow(
|
||||
text: String
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -350,7 +346,7 @@ private fun AboutInstructionRow(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
color = palette.textPrimary
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -453,7 +449,6 @@ private fun AboutFeatureRow(
|
||||
subtitle: String
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -477,14 +472,14 @@ private fun AboutFeatureRow(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
lineHeight = 20.sp,
|
||||
color = palette.textPrimary
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 17.sp,
|
||||
color = palette.textSecondary
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -498,10 +493,10 @@ internal fun BitchatBadge(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
Box(
|
||||
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)
|
||||
) {
|
||||
Text(
|
||||
@ -510,7 +505,7 @@ internal fun BitchatBadge(
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 0.5.sp,
|
||||
color = palette.accentGreen
|
||||
color = colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -556,11 +551,10 @@ internal fun SheetHeaderBadge(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(44.dp)
|
||||
.background(palette.surface, androidx.compose.foundation.shape.CircleShape),
|
||||
.background(colorScheme.surface, androidx.compose.foundation.shape.CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@ -585,8 +579,8 @@ internal fun SheetDestructiveButton(
|
||||
modifier: Modifier = Modifier,
|
||||
isDestructive: Boolean = true
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val accent = if (isDestructive) palette.accentRed else palette.accentGreen
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val accent = if (isDestructive) colorScheme.error else colorScheme.primary
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
|
||||
@ -82,17 +82,17 @@ private fun ThemeChip(
|
||||
onClick: () -> Unit,
|
||||
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
|
||||
// recolouring plus the whole app recolouring underneath it).
|
||||
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),
|
||||
label = "themeChipContainer"
|
||||
)
|
||||
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),
|
||||
label = "themeChipLabel"
|
||||
)
|
||||
@ -146,12 +146,12 @@ private fun SettingsToggleRow(
|
||||
label = "settingsRowIcon"
|
||||
)
|
||||
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),
|
||||
label = "settingsRowTitle"
|
||||
)
|
||||
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),
|
||||
label = "settingsRowSubtitle"
|
||||
)
|
||||
@ -213,9 +213,9 @@ private fun SettingsToggleRow(
|
||||
interactionSource = interactionSource,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = palette.accentGreen,
|
||||
checkedTrackColor = colorScheme.primary,
|
||||
uncheckedThumbColor = Color.White,
|
||||
uncheckedTrackColor = palette.surfaceVariant
|
||||
uncheckedTrackColor = colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -310,7 +310,7 @@ fun AboutSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Row(
|
||||
@ -359,7 +359,7 @@ fun AboutSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
@ -383,7 +383,7 @@ fun AboutSheet(
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 54.dp),
|
||||
thickness = 1.dp,
|
||||
color = palette.outlineVariant
|
||||
color = colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// Proof of Work Toggle
|
||||
@ -398,7 +398,7 @@ fun AboutSheet(
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 54.dp),
|
||||
thickness = 1.dp,
|
||||
color = palette.outlineVariant
|
||||
color = colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// Tor Toggle
|
||||
@ -417,9 +417,9 @@ fun AboutSheet(
|
||||
statusIndicator = if (torMode.value == TorMode.ON) {
|
||||
{
|
||||
val statusColor = when {
|
||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> palette.accentGreen
|
||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> colorScheme.primary
|
||||
torStatus.running -> palette.accentOrange
|
||||
else -> palette.accentRed
|
||||
else -> colorScheme.error
|
||||
}
|
||||
Surface(
|
||||
color = statusColor,
|
||||
@ -811,7 +811,7 @@ fun AboutSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column(
|
||||
@ -828,7 +828,7 @@ fun AboutSheet(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = palette.textPrimary
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
AnimatedCountLabel(
|
||||
count = powDifficulty,
|
||||
@ -839,7 +839,7 @@ fun AboutSheet(
|
||||
),
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 12.sp,
|
||||
color = palette.textSecondary
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@ -849,9 +849,9 @@ fun AboutSheet(
|
||||
valueRange = 0f..32f,
|
||||
steps = 31,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = palette.accentGreen,
|
||||
activeTrackColor = palette.accentGreen,
|
||||
inactiveTrackColor = palette.surfaceVariant
|
||||
thumbColor = colorScheme.primary,
|
||||
activeTrackColor = colorScheme.primary,
|
||||
inactiveTrackColor = colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
|
||||
@ -888,7 +888,7 @@ fun AboutSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column(
|
||||
@ -900,9 +900,9 @@ fun AboutSheet(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val statusColor = when {
|
||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> palette.accentGreen
|
||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> colorScheme.primary
|
||||
torStatus.running -> palette.accentOrange
|
||||
else -> palette.accentRed
|
||||
else -> colorScheme.error
|
||||
}
|
||||
Surface(color = statusColor, shape = CircleShape, modifier = Modifier.size(10.dp)) {}
|
||||
Text(
|
||||
@ -914,7 +914,7 @@ fun AboutSheet(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = palette.textPrimary
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
if (torStatus.lastLogLine.isNotEmpty()) {
|
||||
@ -974,7 +974,7 @@ fun AboutSheet(
|
||||
.align(Alignment.TopCenter)
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.background(palette.background.copy(alpha = topBarAlpha))
|
||||
.background(colorScheme.background.copy(alpha = topBarAlpha))
|
||||
) {
|
||||
val dismiss = LocalSheetDismiss.current
|
||||
CloseButton(
|
||||
|
||||
@ -119,11 +119,12 @@ internal data class TorConnectionVisual(
|
||||
@Composable
|
||||
internal fun rememberTorConnectionVisual(normal: Color): TorConnectionVisual {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val torStatus by remember { ArtiTorManager.getInstance() }.statusFlow.collectAsState()
|
||||
|
||||
// ~28% of the loud accent mixed into the base tint keeps the hue without intensity.
|
||||
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 {
|
||||
torStatus.mode == TorMode.OFF -> TorConnectionVisual(normal, isProgress = false)
|
||||
@ -268,18 +269,19 @@ fun NoiseSessionIcon(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
// 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
|
||||
// palette's secondary text colour.
|
||||
val (icon, color, contentDescription) = when (sessionState) {
|
||||
"uninitialized" -> Triple(
|
||||
Icons.Outlined.NoEncryption,
|
||||
palette.textSecondary,
|
||||
colorScheme.onSurfaceVariant,
|
||||
stringResource(R.string.cd_ready_for_handshake)
|
||||
)
|
||||
"handshaking" -> Triple(
|
||||
Icons.Outlined.Sync,
|
||||
palette.textSecondary,
|
||||
colorScheme.onSurfaceVariant,
|
||||
stringResource(R.string.cd_handshake_in_progress)
|
||||
)
|
||||
"established" -> Triple(
|
||||
@ -290,7 +292,7 @@ fun NoiseSessionIcon(
|
||||
else -> { // "failed" or any other state
|
||||
Triple(
|
||||
Icons.Outlined.Warning,
|
||||
palette.accentRed,
|
||||
colorScheme.error,
|
||||
stringResource(R.string.cd_handshake_failed)
|
||||
)
|
||||
}
|
||||
@ -365,19 +367,20 @@ fun PeerCounter(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
// Compute channel-aware people count and color (matches iOS logic exactly)
|
||||
val (peopleCount, countColor) = when (selectedLocationChannel) {
|
||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||
// Geohash channel: show geohash participants
|
||||
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,
|
||||
null -> {
|
||||
// Mesh channel: show Bluetooth-connected peers (excluding self)
|
||||
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),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontSize = HeaderTextSize,
|
||||
color = if (isConnected) palette.accentGreen else palette.accentRed,
|
||||
color = if (isConnected) colorScheme.primary else colorScheme.error,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
@ -482,7 +485,6 @@ private fun ChannelHeader(
|
||||
onSidebarClick: () -> Unit
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
// 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),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontSize = 15.sp,
|
||||
color = palette.accentRed,
|
||||
color = colorScheme.error,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.clip(HeaderClusterShape)
|
||||
@ -667,7 +669,7 @@ private fun LocationChannelsButton(
|
||||
viewModel: ChatViewModel,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
// Get current channel selection from location manager
|
||||
val selectedChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
@ -680,7 +682,7 @@ private fun LocationChannelsButton(
|
||||
// so it is plain "mesh".
|
||||
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.
|
||||
val torVisual = if (isLocation) {
|
||||
rememberTorConnectionVisual(normal = channelColor)
|
||||
|
||||
@ -46,7 +46,6 @@ import com.bitchat.android.nostr.LocationNotesManager
|
||||
import com.bitchat.android.nostr.NearbyNotesController
|
||||
import com.bitchat.android.ui.media.FullScreenImageViewer
|
||||
import com.bitchat.android.ui.theme.BitchatMotion
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
|
||||
/**
|
||||
* Main ChatScreen - REFACTORED to use component-based architecture
|
||||
@ -61,7 +60,6 @@ import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
@Composable
|
||||
fun ChatScreen(viewModel: ChatViewModel) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
||||
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
@ -415,17 +413,17 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
tonalElevation = 3.dp,
|
||||
shadowElevation = 6.dp,
|
||||
border = BorderStroke(1.dp, palette.accentGreen)
|
||||
border = BorderStroke(1.dp, colorScheme.primary)
|
||||
) {
|
||||
IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowDownward,
|
||||
contentDescription = stringResource(com.bitchat.android.R.string.cd_scroll_to_bottom),
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = palette.accentGreen
|
||||
tint = colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -569,8 +567,6 @@ fun ChatInputSection(
|
||||
showMediaButtons: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Column(
|
||||
// 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
|
||||
@ -581,7 +577,7 @@ fun ChatInputSection(
|
||||
.background(colorScheme.background.copy(alpha = BarBackgroundAlpha))
|
||||
) {
|
||||
// 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
|
||||
if (showCommandSuggestions && commandSuggestions.isNotEmpty()) {
|
||||
@ -590,7 +586,7 @@ fun ChatInputSection(
|
||||
onSuggestionClick = onCommandSuggestionClick,
|
||||
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
|
||||
// suggestions together with visibility; without this snapshot the panel would empty and
|
||||
@ -631,7 +627,7 @@ fun ChatInputSection(
|
||||
onSuggestionClick = onMentionSuggestionClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
||||
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||
}
|
||||
}
|
||||
MessageInput(
|
||||
@ -682,7 +678,6 @@ private fun ChatFloatingHeader(
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) }
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@ -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.BitchatPalette
|
||||
import com.bitchat.android.ui.theme.ChatVisualTokens
|
||||
import com.bitchat.android.ui.theme.colorForPeerSeed
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@ -57,7 +58,11 @@ fun formatTextMessageSender(
|
||||
): AnnotatedString {
|
||||
val builder = AnnotatedString.Builder()
|
||||
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 (baseName, suffix) = splitSuffix(message.sender)
|
||||
|
||||
@ -161,12 +166,12 @@ private fun appendBodyTimestamp(
|
||||
private fun appendMutedTimestamp(
|
||||
builder: AnnotatedString.Builder,
|
||||
message: BitchatMessage,
|
||||
palette: BitchatPalette,
|
||||
contentColor: Color,
|
||||
timeFormatter: SimpleDateFormat,
|
||||
) {
|
||||
builder.pushStyle(
|
||||
SpanStyle(
|
||||
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||
color = contentColor.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||
fontSize = ChatVisualTokens.SystemTimeFontSize,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
@ -186,6 +191,8 @@ fun formatTextMessageBody(
|
||||
message: BitchatMessage,
|
||||
currentUserNickname: String,
|
||||
palette: BitchatPalette,
|
||||
contentColor: Color,
|
||||
linkColor: Color,
|
||||
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
|
||||
includeTimestamp: Boolean = true
|
||||
): AnnotatedString {
|
||||
@ -196,6 +203,8 @@ fun formatTextMessageBody(
|
||||
content = message.content,
|
||||
currentUserNickname = currentUserNickname,
|
||||
palette = palette,
|
||||
contentColor = contentColor,
|
||||
linkColor = linkColor,
|
||||
)
|
||||
|
||||
if (includeTimestamp) {
|
||||
@ -212,13 +221,13 @@ fun formatTextMessageBody(
|
||||
*/
|
||||
fun formatSystemMessage(
|
||||
message: BitchatMessage,
|
||||
palette: BitchatPalette,
|
||||
contentColor: Color,
|
||||
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault())
|
||||
): AnnotatedString {
|
||||
val builder = AnnotatedString.Builder()
|
||||
builder.pushStyle(
|
||||
SpanStyle(
|
||||
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||
color = contentColor.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||
fontSize = ChatVisualTokens.SystemActionFontSize,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
@ -227,7 +236,7 @@ fun formatSystemMessage(
|
||||
builder.append(message.content)
|
||||
builder.pop()
|
||||
|
||||
appendMutedTimestamp(builder, message, palette, timeFormatter)
|
||||
appendMutedTimestamp(builder, message, contentColor, timeFormatter)
|
||||
return builder.toAnnotatedString()
|
||||
}
|
||||
|
||||
@ -243,6 +252,7 @@ fun formatMessageHeaderAnnotatedString(
|
||||
currentUserNickname: String,
|
||||
myPeerID: String,
|
||||
palette: BitchatPalette,
|
||||
contentColor: Color,
|
||||
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
|
||||
includeSender: Boolean = true
|
||||
): AnnotatedString {
|
||||
@ -250,11 +260,15 @@ fun formatMessageHeaderAnnotatedString(
|
||||
val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
|
||||
|
||||
if (message.sender == "system") {
|
||||
return formatSystemMessage(message, palette, timeFormatter)
|
||||
return formatSystemMessage(message, contentColor, timeFormatter)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
builder.pushStyle(
|
||||
@ -291,71 +305,10 @@ fun formatMessageHeaderAnnotatedString(
|
||||
}
|
||||
}
|
||||
|
||||
appendMutedTimestamp(builder, message, palette, timeFormatter)
|
||||
appendMutedTimestamp(builder, message, contentColor, timeFormatter)
|
||||
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)
|
||||
*/
|
||||
@ -387,7 +340,7 @@ internal fun isUnannouncedNickname(displayName: String): Boolean {
|
||||
* iOS-style content formatting with proper hashtag and mention handling.
|
||||
*
|
||||
* 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.
|
||||
* - 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
|
||||
@ -401,10 +354,9 @@ private fun appendIOSFormattedContent(
|
||||
content: String,
|
||||
currentUserNickname: String,
|
||||
palette: BitchatPalette,
|
||||
contentColor: Color,
|
||||
linkColor: Color,
|
||||
) {
|
||||
val contentColor = palette.textPrimary
|
||||
val linkColor = palette.accentBlue
|
||||
|
||||
// iOS-style patterns: allow optional '#abcd' suffix in mentions
|
||||
val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex()
|
||||
val mentionPattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)".toRegex()
|
||||
@ -511,7 +463,10 @@ private fun appendIOSFormattedContent(
|
||||
palette.accentOrange
|
||||
} else {
|
||||
// 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 mentionWeight = if (isMentionToMe) FontWeight.Bold else FontWeight.SemiBold
|
||||
|
||||
@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitchat.android.R
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
@ -35,14 +36,13 @@ fun ChatUserSheet(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
// iOS system colors (matches LocationChannelsSheet exactly)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) // iOS green
|
||||
val standardBlue = Color(0xFF007AFF) // iOS blue
|
||||
val standardPurple = if (isDark) Color(0xFFBF5AF2) else Color(0xFFAF52DE) // iOS purple
|
||||
val standardRed = Color(0xFFFF3B30) // iOS red
|
||||
val standardGrey = if (isDark) Color(0xFF8E8E93) else Color(0xFF6D6D70) // iOS grey
|
||||
val palette = LocalBitchatPalette.current
|
||||
val standardGreen = colorScheme.primary
|
||||
val standardBlue = colorScheme.secondary
|
||||
val standardPurple = palette.accentPurple
|
||||
val standardRed = colorScheme.error
|
||||
val standardGrey = colorScheme.onSurfaceVariant
|
||||
|
||||
if (isPresented) {
|
||||
BitchatBottomSheet(
|
||||
|
||||
@ -1195,17 +1195,12 @@ class ChatViewModel(
|
||||
/**
|
||||
* Get consistent color for a mesh peer by ID (iOS-compatible)
|
||||
*/
|
||||
fun colorForMeshPeer(peerID: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
|
||||
// Try to get stable Noise key, fallback to peer ID
|
||||
val seed = "noise:${peerID.lowercase()}"
|
||||
return colorForPeerSeed(seed, isDark).copy()
|
||||
}
|
||||
fun peerColorSeedForMeshPeer(peerID: String): PeerColorSeed = meshPeerColorSeed(peerID)
|
||||
|
||||
/**
|
||||
* Get consistent color for a Nostr pubkey (iOS-compatible)
|
||||
*/
|
||||
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
|
||||
return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark)
|
||||
}
|
||||
fun peerColorSeedForNostrPubkey(pubkeyHex: String): PeerColorSeed =
|
||||
geohashViewModel.peerColorSeedForNostrPubkey(pubkeyHex)
|
||||
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||
import com.bitchat.android.ui.theme.colorForPeerSeed
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
import java.util.*
|
||||
@ -45,6 +46,7 @@ fun GeohashPeopleList(
|
||||
val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val myHex = remember(selectedLocationChannel) {
|
||||
when (val channel = selectedLocationChannel) {
|
||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||
@ -110,7 +112,7 @@ fun GeohashPeopleList(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Text(
|
||||
@ -212,14 +214,14 @@ private fun PeopleCard(
|
||||
people: List<GeoPerson>,
|
||||
row: @Composable (GeoPerson) -> Unit
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
AnimatedRowColumn(items = people, key = { it.id }) { index, person ->
|
||||
@ -250,7 +252,10 @@ private fun GeohashPersonItem(
|
||||
val (baseNameRaw, suffixRaw) = splitSuffix(person.displayName)
|
||||
val baseName = truncateNickname(baseNameRaw)
|
||||
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
|
||||
|
||||
Row(
|
||||
|
||||
@ -30,10 +30,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitchat.android.ui.theme.BitchatTheme
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -87,15 +87,13 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
val initialPrecision = geohashToFocus?.length ?: 5
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
BitchatTheme {
|
||||
var currentGeohash by remember { mutableStateOf(geohashToFocus ?: "") }
|
||||
var precision by remember { mutableStateOf(initialPrecision.coerceIn(1, 12)) }
|
||||
var webViewRef by remember { mutableStateOf<WebView?>(null) }
|
||||
|
||||
// iOS system-like colors used across app
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
|
||||
val standardGreen = colorScheme.primary
|
||||
|
||||
Scaffold { padding ->
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
|
||||
@ -332,10 +332,8 @@ class GeohashViewModel(
|
||||
fun displayNameForNostrPubkeyUI(pubkeyHex: String): String = repo.displayNameForNostrPubkeyUI(pubkeyHex)
|
||||
fun displayNameForGeohashConversation(pubkeyHex: String, sourceGeohash: String): String = repo.displayNameForGeohashConversation(pubkeyHex, sourceGeohash)
|
||||
|
||||
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
|
||||
val seed = "nostr:${pubkeyHex.lowercase()}"
|
||||
return colorForPeerSeed(seed, isDark).copy()
|
||||
}
|
||||
fun peerColorSeedForNostrPubkey(pubkeyHex: String): PeerColorSeed =
|
||||
nostrPeerColorSeed(pubkeyHex)
|
||||
|
||||
private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
|
||||
geoTimer?.cancel(); geoTimer = null
|
||||
|
||||
@ -257,7 +257,8 @@ internal fun ComposerActionSurface(
|
||||
content: @Composable (tint: Color) -> Unit
|
||||
) {
|
||||
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(
|
||||
// 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"
|
||||
)
|
||||
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),
|
||||
label = "composerButtonTint"
|
||||
)
|
||||
@ -314,6 +315,7 @@ fun MessageInput(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isFocused = remember { mutableStateOf(false) }
|
||||
val hasText = value.text.isNotBlank()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@ -324,7 +326,7 @@ fun MessageInput(
|
||||
// Recording is the one state worth shouting about, so it overrides focus.
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
isRecording -> palette.accentRed.copy(alpha = 0.7f)
|
||||
isRecording -> colorScheme.error.copy(alpha = 0.7f)
|
||||
isFocused.value -> palette.inputOutlineFocused
|
||||
else -> palette.inputOutline
|
||||
},
|
||||
@ -371,11 +373,11 @@ fun MessageInput(
|
||||
// user is composing rather than reading, and green-on-black is tiring to
|
||||
// type into.
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = palette.textPrimary,
|
||||
color = colorScheme.onSurface,
|
||||
fontFamily = BitchatFontFamily
|
||||
),
|
||||
cursorBrush = SolidColor(
|
||||
if (isRecording) Color.Transparent else palette.textPrimary
|
||||
if (isRecording) Color.Transparent else colorScheme.onSurface
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
||||
keyboardActions = KeyboardActions(onSend = {
|
||||
@ -387,8 +389,8 @@ fun MessageInput(
|
||||
CombinedVisualTransformation(
|
||||
listOf(
|
||||
SlashCommandVisualTransformation(
|
||||
commandColor = palette.accentGreen,
|
||||
commandBackground = palette.accentGreen.copy(alpha = 0.14f),
|
||||
commandColor = colorScheme.primary,
|
||||
commandBackground = colorScheme.primary.copy(alpha = 0.14f),
|
||||
),
|
||||
MentionVisualTransformation(
|
||||
mentionColor = palette.accentOrange,
|
||||
@ -452,7 +454,7 @@ fun MessageInput(
|
||||
secs / 60, secs % 60, maxSecs / 60, maxSecs % 60
|
||||
),
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.accentRed,
|
||||
color = colorScheme.error,
|
||||
fontSize = (BASE_FONT_SIZE - 4).sp
|
||||
)
|
||||
}
|
||||
@ -595,6 +597,7 @@ private fun SendButton(
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
|
||||
@ -602,7 +605,7 @@ private fun SendButton(
|
||||
isActive = enabled,
|
||||
isPressed = isPressed,
|
||||
// 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(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
@ -624,13 +627,13 @@ fun CommandSuggestionsBox(
|
||||
onSuggestionClick: (CommandSuggestion) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.background(palette.surface)
|
||||
.border(1.dp, palette.outlineVariant, RoundedCornerShape(8.dp))
|
||||
.background(colorScheme.surface)
|
||||
.border(1.dp, colorScheme.outlineVariant, RoundedCornerShape(8.dp))
|
||||
.padding(vertical = 6.dp)
|
||||
) {
|
||||
suggestions.forEach { suggestion: CommandSuggestion ->
|
||||
@ -683,7 +686,7 @@ fun CommandSuggestionItem(
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = BitchatFontFamily
|
||||
),
|
||||
color = palette.textSecondary,
|
||||
color = colorScheme.onSurfaceVariant,
|
||||
fontSize = (BASE_FONT_SIZE - 4).sp
|
||||
)
|
||||
}
|
||||
@ -709,7 +712,7 @@ fun MentionSuggestionsBox(
|
||||
onSuggestionClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
@ -721,8 +724,8 @@ fun MentionSuggestionsBox(
|
||||
)
|
||||
)
|
||||
.clip(MentionSuggestionsShape)
|
||||
.background(palette.surface)
|
||||
.border(1.dp, palette.outlineVariant, MentionSuggestionsShape),
|
||||
.background(colorScheme.surface)
|
||||
.border(1.dp, colorScheme.outlineVariant, MentionSuggestionsShape),
|
||||
contentPadding = PaddingValues(vertical = MentionSuggestionsVerticalPadding)
|
||||
) {
|
||||
items(
|
||||
|
||||
@ -43,11 +43,8 @@ fun LinkPreviewPill(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||
|
||||
// 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 textColor = colorScheme.secondary
|
||||
val backgroundColor = colorScheme.secondaryContainer.copy(alpha = 0.35f)
|
||||
val borderColor = textColor.copy(alpha = 0.3f)
|
||||
|
||||
// Parse URL for host extraction
|
||||
@ -87,7 +84,7 @@ fun LinkPreviewPill(
|
||||
Surface(
|
||||
modifier = Modifier.size(60.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = Color.Blue.copy(alpha = 0.1f)
|
||||
color = colorScheme.secondaryContainer
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@ -97,7 +94,7 @@ fun LinkPreviewPill(
|
||||
imageVector = Icons.Outlined.Link,
|
||||
contentDescription = stringResource(com.bitchat.android.R.string.cd_link),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Blue
|
||||
tint = colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.RoundedCornerShape
|
||||
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.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.wifiaware.WifiAwareController
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
/**
|
||||
* 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 palette = LocalBitchatPalette.current
|
||||
val standardGreen = palette.accentGreen
|
||||
val standardBlue = palette.accentBlue
|
||||
val standardGreen = colorScheme.primary
|
||||
val standardBlue = colorScheme.secondary
|
||||
|
||||
val nearbyChannels = remember(availableChannels) {
|
||||
availableChannels.filter { it.level != GeohashChannelLevel.BUILDING }
|
||||
@ -185,7 +190,12 @@ fun LocationChannelsSheet(
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
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),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
@ -203,7 +213,7 @@ fun LocationChannelsSheet(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
ChannelOptionRow(
|
||||
@ -253,7 +263,7 @@ fun LocationChannelsSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
ChannelOptionRow(
|
||||
@ -292,7 +302,7 @@ fun LocationChannelsSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
@ -353,7 +363,7 @@ fun LocationChannelsSheet(
|
||||
text = stringResource(R.string.location_permission_denied),
|
||||
fontSize = 12.sp,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.accentRed
|
||||
color = colorScheme.error
|
||||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
@ -389,7 +399,7 @@ fun LocationChannelsSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
@ -443,12 +453,7 @@ fun LocationChannelsSheet(
|
||||
customError = null
|
||||
},
|
||||
onFocusGained = {
|
||||
coroutineScope.launch {
|
||||
sheetState.expand()
|
||||
listState.animateScrollToItem(
|
||||
index = listState.layoutInfo.totalItemsCount - 1
|
||||
)
|
||||
}
|
||||
coroutineScope.launch { sheetState.expand() }
|
||||
},
|
||||
onOpenMap = {
|
||||
val normalized = customGeohash.trim().lowercase().replace("#", "")
|
||||
@ -497,7 +502,7 @@ fun LocationChannelsSheet(
|
||||
text = shownError,
|
||||
fontSize = 12.sp,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.accentRed,
|
||||
color = colorScheme.error,
|
||||
modifier = Modifier.padding(
|
||||
start = AboutHorizontalPadding + ChannelRowHorizontal,
|
||||
top = 8.dp
|
||||
@ -518,7 +523,7 @@ fun LocationChannelsSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
ChannelSettingsToggleRow(
|
||||
@ -638,7 +643,6 @@ private fun ChannelOptionRow(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
val (baseTitle, countSuffix) = splitTitleAndCount(title)
|
||||
|
||||
Row(
|
||||
@ -657,7 +661,7 @@ private fun ChannelOptionRow(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(ChannelSelectedDot)
|
||||
.background(palette.accentGreen, CircleShape)
|
||||
.background(colorScheme.primary, CircleShape)
|
||||
)
|
||||
}
|
||||
leadingIcon != null -> {
|
||||
@ -686,7 +690,7 @@ private fun ChannelOptionRow(
|
||||
fontSize = 14.sp,
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontWeight = if (titleBold) FontWeight.SemiBold else FontWeight.Medium,
|
||||
color = titleColor ?: palette.textPrimary
|
||||
color = titleColor ?: colorScheme.onSurface
|
||||
)
|
||||
countSuffix?.let { count ->
|
||||
AnimatedCountLabel(
|
||||
@ -694,7 +698,7 @@ private fun ChannelOptionRow(
|
||||
text = count,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.textSecondary
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -703,7 +707,7 @@ private fun ChannelOptionRow(
|
||||
fontSize = 12.sp,
|
||||
fontFamily = BitchatFontFamily,
|
||||
lineHeight = 17.sp,
|
||||
color = palette.textSecondary
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@ -719,7 +723,7 @@ private fun ChannelBookmarkButton(
|
||||
bookmarked: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
@ -737,7 +741,7 @@ private fun ChannelBookmarkButton(
|
||||
contentDescription = stringResource(
|
||||
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)
|
||||
)
|
||||
}
|
||||
@ -745,7 +749,7 @@ private fun ChannelBookmarkButton(
|
||||
|
||||
@Composable
|
||||
private fun ChannelLoadingRow() {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@ -760,14 +764,14 @@ private fun ChannelLoadingRow() {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = palette.textSecondary
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.finding_nearby_channels),
|
||||
fontSize = 12.sp,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.textSecondary
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -785,6 +789,10 @@ private fun CustomGeohashRow(
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
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 isValid = validateGeohash(normalized)
|
||||
// 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) {
|
||||
colorScheme.primary.copy(alpha = 0.16f)
|
||||
} else {
|
||||
palette.surfaceVariant
|
||||
colorScheme.surfaceVariant
|
||||
},
|
||||
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
||||
label = "teleportContainer"
|
||||
@ -809,6 +817,7 @@ private fun CustomGeohashRow(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
.padding(horizontal = ChannelRowHorizontal, vertical = ChannelRowVertical),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@ -847,7 +856,22 @@ private fun CustomGeohashRow(
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.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 ->
|
||||
if (customGeohash.isEmpty()) {
|
||||
Text(
|
||||
@ -871,7 +895,7 @@ private fun CustomGeohashRow(
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Map,
|
||||
contentDescription = stringResource(R.string.cd_open_map),
|
||||
tint = palette.textSecondary,
|
||||
tint = colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
@ -937,7 +961,7 @@ private fun ChannelSettingsToggleRow(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (enabled) palette.textPrimary else palette.textTertiary
|
||||
color = if (enabled) colorScheme.onSurface else palette.textTertiary
|
||||
)
|
||||
statusIndicator?.invoke()
|
||||
}
|
||||
@ -946,7 +970,7 @@ private fun ChannelSettingsToggleRow(
|
||||
fontFamily = BitchatFontFamily,
|
||||
fontSize = 12.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))
|
||||
@ -956,9 +980,9 @@ private fun ChannelSettingsToggleRow(
|
||||
enabled = enabled,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = palette.accentGreen,
|
||||
checkedTrackColor = colorScheme.primary,
|
||||
uncheckedThumbColor = Color.White,
|
||||
uncheckedTrackColor = palette.surfaceVariant
|
||||
uncheckedTrackColor = colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,7 +21,6 @@ import com.bitchat.android.R
|
||||
import com.bitchat.android.geohash.ChannelID
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.nostr.LocationNotesManager
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
|
||||
/**
|
||||
* Location Notes button for MainHeader.
|
||||
@ -35,7 +34,6 @@ fun LocationNotesButton(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
@ -54,7 +52,7 @@ fun LocationNotesButton(
|
||||
if (selectedLocationChannel is ChannelID.Mesh && locationEnabled) {
|
||||
val hasNotes = notesCount > 0
|
||||
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)
|
||||
|
||||
Box(
|
||||
|
||||
@ -5,7 +5,6 @@ import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@ -51,10 +50,8 @@ fun LocationNotesSheet(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
||||
// iOS color scheme
|
||||
val accentGreen = if (isDark) Color.Green else Color(0xFF008000) // dark: green, light: dark green (0, 0.5, 0)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val accentGreen = colorScheme.primary
|
||||
|
||||
// Managers
|
||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||
@ -468,7 +465,6 @@ private fun LocationNotesInputSection(
|
||||
nickname: String?,
|
||||
onSend: () -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
Column(
|
||||
@ -556,10 +552,8 @@ private fun LocationNotesInputSection(
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = if (!sendButtonEnabled) {
|
||||
colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
} else if (isDark) {
|
||||
Color.Black // Black arrow on green in dark theme
|
||||
} else {
|
||||
Color.White // White arrow on green in light theme
|
||||
colorScheme.onPrimary
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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.BitchatMotion
|
||||
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.GeohashConversationRegistry
|
||||
import com.bitchat.android.services.ContactDirectory
|
||||
@ -132,7 +133,7 @@ fun MeshPeerListSheet(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = LocalBitchatPalette.current.surface,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
@ -222,7 +223,7 @@ fun MeshPeerListSheet(
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.QrCode,
|
||||
contentDescription = stringResource(R.string.verify_title),
|
||||
tint = LocalBitchatPalette.current.textSecondary,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
@ -265,7 +266,7 @@ private fun ChannelRow(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(SheetRowSelectedDot)
|
||||
.background(palette.accentGreen, CircleShape)
|
||||
.background(colorScheme.primary, CircleShape)
|
||||
)
|
||||
} else if (unreadCount > 0) {
|
||||
UnreadBadge(count = unreadCount, colorScheme = colorScheme)
|
||||
@ -286,7 +287,7 @@ private fun ChannelRow(
|
||||
text = channel,
|
||||
fontFamily = BitchatFontFamily,
|
||||
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,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
@ -331,7 +332,7 @@ fun PeopleSection(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
color = colorScheme.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
@ -588,7 +589,10 @@ private fun PeerItem(
|
||||
|
||||
// Get consistent peer color (iOS-compatible)
|
||||
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
|
||||
|
||||
Row(
|
||||
@ -606,7 +610,7 @@ private fun PeerItem(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(SheetRowSelectedDot)
|
||||
.background(palette.accentGreen, CircleShape)
|
||||
.background(colorScheme.primary, CircleShape)
|
||||
)
|
||||
} else if (hasUnreadDM) {
|
||||
Icon(
|
||||
@ -642,7 +646,7 @@ private fun PeerItem(
|
||||
else -> "Routed"
|
||||
},
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.textSecondary
|
||||
tint = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -679,7 +683,7 @@ private fun PeerItem(
|
||||
imageVector = Icons.Filled.Verified,
|
||||
contentDescription = stringResource(R.string.verify_title),
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = palette.accentGreen
|
||||
tint = colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -866,7 +870,6 @@ fun PrivateChatSheet(
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
if (isPresented) {
|
||||
BitchatBottomSheet(
|
||||
@ -879,7 +882,7 @@ fun PrivateChatSheet(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
|
||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
||||
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||
|
||||
// Messages list
|
||||
var forceScrollToBottom by remember { mutableStateOf(false) }
|
||||
|
||||
@ -498,6 +498,7 @@ fun MessageItem(
|
||||
currentUserNickname = currentUserNickname,
|
||||
myPeerID = meshService.myPeerID,
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
timeFormatter = timeFormatter,
|
||||
includeSender = showSender
|
||||
)
|
||||
@ -516,7 +517,7 @@ fun MessageItem(
|
||||
},
|
||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.textPrimary,
|
||||
color = colorScheme.onSurface,
|
||||
)
|
||||
|
||||
// Try to load the file packet from the path
|
||||
@ -564,14 +565,14 @@ fun MessageItem(
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(4.dp)
|
||||
.size(22.dp)
|
||||
.background(palette.surfaceVariant.copy(alpha = 0.85f), CircleShape)
|
||||
.background(colorScheme.surfaceVariant.copy(alpha = 0.85f), CircleShape)
|
||||
.clickable { onCancelTransfer?.invoke(message) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(R.string.cd_cancel),
|
||||
tint = palette.textPrimary,
|
||||
tint = colorScheme.onSurface,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
@ -591,10 +592,10 @@ fun MessageItem(
|
||||
|
||||
if (message.sender == "system") {
|
||||
// Background narration: `// Tor started. Routing all chats…`
|
||||
val annotatedText = remember(message, palette) {
|
||||
val annotatedText = remember(message, colorScheme.onSurface) {
|
||||
formatSystemMessage(
|
||||
message = message,
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
timeFormatter = timeFormatter
|
||||
)
|
||||
}
|
||||
@ -614,7 +615,7 @@ fun MessageItem(
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible,
|
||||
style = ChatVisualTokens.SystemActionStyle.copy(
|
||||
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||
color = colorScheme.onSurface.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@ -660,11 +661,20 @@ internal fun TextMessageLayout(
|
||||
}
|
||||
// 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.
|
||||
val bodyText = remember(displayMessage, currentUserNickname, palette, timeFormatter) {
|
||||
val bodyText = remember(
|
||||
displayMessage,
|
||||
currentUserNickname,
|
||||
palette,
|
||||
colorScheme.onSurface,
|
||||
colorScheme.secondary,
|
||||
timeFormatter
|
||||
) {
|
||||
formatTextMessageBody(
|
||||
message = displayMessage,
|
||||
currentUserNickname = currentUserNickname,
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
)
|
||||
}
|
||||
@ -728,7 +738,7 @@ internal fun TextMessageLayout(
|
||||
fontFamily = BitchatFontFamily,
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible,
|
||||
style = MessageBodyTextStyle.copy(color = palette.textPrimary),
|
||||
style = MessageBodyTextStyle.copy(color = colorScheme.onSurface),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -736,7 +746,6 @@ internal fun TextMessageLayout(
|
||||
@Composable
|
||||
fun DeliveryStatusIcon(status: DeliveryStatus) {
|
||||
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.
|
||||
// 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 ->
|
||||
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.8f), FontWeight.Normal)
|
||||
DeliveryStatus.Read::class ->
|
||||
Triple(R.string.status_delivered, palette.accentBlue, FontWeight.Bold)
|
||||
Triple(R.string.status_delivered, colorScheme.secondary, FontWeight.Bold)
|
||||
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.
|
||||
else ->
|
||||
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal)
|
||||
|
||||
33
app/src/main/java/com/bitchat/android/ui/PeerColorSeed.kt
Normal file
33
app/src/main/java/com/bitchat/android/ui/PeerColorSeed.kt
Normal 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)
|
||||
}
|
||||
@ -10,7 +10,6 @@ import androidx.compose.material.icons.outlined.Warning as OutlinedWarning
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -70,9 +69,9 @@ fun SecurityVerificationSheet(
|
||||
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
|
||||
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val accent = if (isDark) Color.Green else Color(0xFF008000)
|
||||
val boxColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.06f)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val accent = colorScheme.primary
|
||||
val boxColor = colorScheme.surfaceVariant
|
||||
val peerHexRegex = remember { Regex("^[0-9a-fA-F]{16}$") }
|
||||
|
||||
BitchatBottomSheet(
|
||||
|
||||
@ -19,7 +19,6 @@ import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -99,8 +98,7 @@ fun VerificationSheet(
|
||||
) {
|
||||
if (!isPresented) return
|
||||
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val accent = if (isDark) Color.Green else Color(0xFF008000)
|
||||
val accent = MaterialTheme.colorScheme.primary
|
||||
|
||||
var selectedTab by remember { mutableStateOf(0) } // 0 = My Code, 1 = Scan
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
|
||||
@ -55,6 +55,7 @@ fun AudioMessageItem(
|
||||
currentUserNickname = currentUserNickname,
|
||||
myPeerID = meshService.myPeerID,
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
timeFormatter = timeFormatter,
|
||||
includeSender = showSender
|
||||
)
|
||||
@ -73,7 +74,7 @@ fun AudioMessageItem(
|
||||
},
|
||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.textPrimary,
|
||||
color = colorScheme.onSurface,
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
@ -53,6 +53,7 @@ fun ImageMessageItem(
|
||||
currentUserNickname = currentUserNickname,
|
||||
myPeerID = meshService.myPeerID,
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
timeFormatter = timeFormatter,
|
||||
includeSender = showSender
|
||||
)
|
||||
@ -71,7 +72,7 @@ fun ImageMessageItem(
|
||||
},
|
||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = palette.textPrimary,
|
||||
color = colorScheme.onSurface,
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
@ -5,40 +5,18 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
||||
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:
|
||||
* `onSurface` is the terminal green used for chrome, while message bodies must render in a
|
||||
* neutral near-white. Rather than repurposing M3 slots (which would repaint ~40 unrelated
|
||||
* 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.
|
||||
* Standard backgrounds, surfaces, text, outlines, primary/secondary accents, and errors belong
|
||||
* to [androidx.compose.material3.MaterialTheme.colorScheme]. Keeping only the extra app semantics
|
||||
* here lets Material components inherit correct defaults without losing Bitchat's identity.
|
||||
*/
|
||||
@Immutable
|
||||
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
|
||||
/**
|
||||
* 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
|
||||
* cast there made the whole bottom of the screen read as "terminal chrome" instead of a
|
||||
* place to write.
|
||||
* Material outline: the composer is the one surface the user stares at while typing.
|
||||
*/
|
||||
val inputOutline: Color,
|
||||
/** 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. */
|
||||
val inputButton: Color,
|
||||
|
||||
// MARK: - Text
|
||||
/** Message bodies and row titles. Neutral, not green. */
|
||||
val textPrimary: Color,
|
||||
/** Subtitles, secondary metadata, system/action messages. */
|
||||
val textSecondary: Color,
|
||||
// MARK: - Extra semantics
|
||||
/** Timestamps, placeholders, section labels, disabled states. */
|
||||
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. */
|
||||
val accentOrange: Color,
|
||||
/** Errors, warning card, destructive actions. */
|
||||
val accentRed: Color,
|
||||
/** Nostr reachability. */
|
||||
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(
|
||||
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),
|
||||
inputOutlineFocused = Color(0xFF5A605D),
|
||||
inputSurface = Color(0xFF0B0B0B),
|
||||
inputSurfaceFocused = Color(0xFF151515),
|
||||
inputButton = Color(0xFF1E1E1E),
|
||||
textPrimary = Color(0xFFF5F5F5),
|
||||
textSecondary = Color(0xFF9AA69A),
|
||||
textTertiary = Color(0xFF6B776B),
|
||||
accentGreen = Color(0xFF32D74B),
|
||||
accentBlue = Color(0xFF0A84FF),
|
||||
accentOrange = Color(0xFFFF9F0A),
|
||||
accentRed = Color(0xFFFF453A),
|
||||
accentPurple = Color(0xFFBF5AF2),
|
||||
peerColorSaturation = 1f,
|
||||
peerColorValue = 1f,
|
||||
)
|
||||
|
||||
val LightBitchatPalette = BitchatPalette(
|
||||
isDark = false,
|
||||
background = Color(0xFFFFFFFF),
|
||||
surface = Color(0xFFF2F6F2),
|
||||
surfaceVariant = Color(0xFFE7EDE7),
|
||||
outline = Color(0xFFCBD6CB),
|
||||
outlineVariant = Color(0xFFDEE6DE),
|
||||
inputOutline = Color(0xFFCFD3D1),
|
||||
inputOutlineFocused = Color(0xFF8E9490),
|
||||
inputSurface = Color(0xFFFAFAFA),
|
||||
inputSurfaceFocused = Color(0xFFF2F2F2),
|
||||
inputButton = Color(0xFFE8E8E8),
|
||||
textPrimary = Color(0xFF131A13),
|
||||
textSecondary = Color(0xFF4C574C),
|
||||
textTertiary = Color(0xFF757F75),
|
||||
accentGreen = Color(0xFF248A3D),
|
||||
accentBlue = Color(0xFF007AFF),
|
||||
accentOrange = Color(0xFFFF9500),
|
||||
accentRed = Color(0xFFD70015),
|
||||
accentPurple = Color(0xFFAF52DE),
|
||||
peerColorSaturation = 0.85f,
|
||||
peerColorValue = 0.45f,
|
||||
)
|
||||
|
||||
val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette }
|
||||
|
||||
30
app/src/main/java/com/bitchat/android/ui/theme/PeerColors.kt
Normal file
30
app/src/main/java/com/bitchat/android/ui/theme/PeerColors.kt
Normal 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
|
||||
)
|
||||
}
|
||||
@ -17,40 +17,51 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
|
||||
// Colors that match the iOS bitchat theme.
|
||||
// Surface/outline/error slots are derived from BitchatPalette so that screens which have not
|
||||
// yet migrated to the explicit palette still pick up the redesigned tinted surfaces.
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Color(0xFF39FF14), // Bright green (terminal-like)
|
||||
// Standard UI semantics live in Material so stock components and custom Bitchat composables
|
||||
// share one source of truth. LocalBitchatPalette below only supplies app-specific extra colors.
|
||||
internal val DarkBitchatColorScheme = darkColorScheme(
|
||||
primary = Color(0xFF32D74B),
|
||||
onPrimary = Color.Black,
|
||||
secondary = Color(0xFF2ECB10), // Darker green
|
||||
primaryContainer = Color(0xFF163D1D),
|
||||
onPrimaryContainer = Color(0xFFB8F5C1),
|
||||
secondary = Color(0xFF0A84FF),
|
||||
onSecondary = Color.Black,
|
||||
background = DarkBitchatPalette.background,
|
||||
onBackground = Color(0xFF39FF14), // Green on near-black
|
||||
surface = DarkBitchatPalette.surface,
|
||||
onSurface = Color(0xFF39FF14), // Green text
|
||||
surfaceVariant = DarkBitchatPalette.surfaceVariant,
|
||||
onSurfaceVariant = DarkBitchatPalette.textSecondary,
|
||||
outline = DarkBitchatPalette.outline,
|
||||
outlineVariant = DarkBitchatPalette.outlineVariant,
|
||||
error = DarkBitchatPalette.accentRed,
|
||||
secondaryContainer = Color(0xFF082E54),
|
||||
onSecondaryContainer = Color(0xFFC2E0FF),
|
||||
tertiary = DarkBitchatPalette.accentOrange,
|
||||
onTertiary = Color.Black,
|
||||
background = Color(0xFF000000),
|
||||
onBackground = Color(0xFFF5F5F5),
|
||||
surface = Color(0xFF0E150E),
|
||||
onSurface = Color(0xFFF5F5F5),
|
||||
surfaceVariant = Color(0xFF182118),
|
||||
onSurfaceVariant = Color(0xFF9AA69A),
|
||||
outline = Color(0xFF2A3A2A),
|
||||
outlineVariant = Color(0xFF1C271C),
|
||||
error = Color(0xFFFF453A),
|
||||
onError = Color.Black
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Color(0xFF008000), // Dark green
|
||||
internal val LightBitchatColorScheme = lightColorScheme(
|
||||
primary = Color(0xFF248A3D),
|
||||
onPrimary = Color.White,
|
||||
secondary = Color(0xFF006600), // Even darker green
|
||||
primaryContainer = Color(0xFFD5F1D8),
|
||||
onPrimaryContainer = Color(0xFF0A3212),
|
||||
secondary = Color(0xFF007AFF),
|
||||
onSecondary = Color.White,
|
||||
background = LightBitchatPalette.background,
|
||||
onBackground = Color(0xFF008000), // Dark green on off-white
|
||||
surface = LightBitchatPalette.surface,
|
||||
onSurface = Color(0xFF008000), // Dark green text
|
||||
surfaceVariant = LightBitchatPalette.surfaceVariant,
|
||||
onSurfaceVariant = LightBitchatPalette.textSecondary,
|
||||
outline = LightBitchatPalette.outline,
|
||||
outlineVariant = LightBitchatPalette.outlineVariant,
|
||||
error = LightBitchatPalette.accentRed,
|
||||
secondaryContainer = Color(0xFFD6E9FF),
|
||||
onSecondaryContainer = Color(0xFF002C5C),
|
||||
tertiary = LightBitchatPalette.accentOrange,
|
||||
onTertiary = Color.Black,
|
||||
background = Color(0xFFFFFFFF),
|
||||
onBackground = Color(0xFF131A13),
|
||||
surface = Color(0xFFF2F6F2),
|
||||
onSurface = Color(0xFF131A13),
|
||||
surfaceVariant = Color(0xFFE7EDE7),
|
||||
onSurfaceVariant = Color(0xFF4C574C),
|
||||
outline = Color(0xFFCBD6CB),
|
||||
outlineVariant = Color(0xFFDEE6DE),
|
||||
error = Color(0xFFD70015),
|
||||
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 view = LocalView.current
|
||||
|
||||
@ -7,16 +7,18 @@ import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||
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.LightBitchatColorScheme
|
||||
import com.bitchat.android.ui.theme.LightBitchatPalette
|
||||
import com.bitchat.android.ui.theme.MessageBodyTextStyle
|
||||
import com.bitchat.android.ui.theme.MessageSenderTextStyle
|
||||
import com.bitchat.android.ui.theme.colorForPeerSeed
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@ -33,6 +35,7 @@ class ChatUIUtilsTest {
|
||||
}
|
||||
|
||||
private val palette = DarkBitchatPalette
|
||||
private val colorScheme = DarkBitchatColorScheme
|
||||
|
||||
private fun message(
|
||||
content: String,
|
||||
@ -71,6 +74,8 @@ class ChatUIUtilsTest {
|
||||
message = message("hello there"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
)
|
||||
|
||||
@ -89,6 +94,8 @@ class ChatUIUtilsTest {
|
||||
message = message("mined", powDifficulty = 8),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
)
|
||||
|
||||
@ -106,6 +113,8 @@ class ChatUIUtilsTest {
|
||||
message = message("hello there"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
includeTimestamp = false,
|
||||
)
|
||||
@ -119,12 +128,14 @@ class ChatUIUtilsTest {
|
||||
message = message("plain words"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
includeTimestamp = false,
|
||||
)
|
||||
|
||||
val textStyle = body.spanStyles.first { it.start == 0 }
|
||||
assertEquals(palette.textPrimary, textStyle.item.color)
|
||||
assertEquals(colorScheme.onSurface, textStyle.item.color)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -146,6 +157,8 @@ class ChatUIUtilsTest {
|
||||
message = message("my own words", sender = "bob"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
includeTimestamp = false,
|
||||
)
|
||||
@ -170,6 +183,8 @@ class ChatUIUtilsTest {
|
||||
message = message("hey @carol#04af what's up"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
includeTimestamp = false,
|
||||
)
|
||||
@ -188,6 +203,8 @@ class ChatUIUtilsTest {
|
||||
message = message("ping @bob now"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
includeTimestamp = false,
|
||||
)
|
||||
@ -208,11 +225,13 @@ class ChatUIUtilsTest {
|
||||
message = message("cc @carol#04af"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
includeTimestamp = false,
|
||||
)
|
||||
|
||||
val expected = colorForPeerSeed("carol#04af", palette.isDark)
|
||||
val expected = colorForPeerSeed(PeerColorSeed("carol#04af"), palette)
|
||||
val chip = mentionChipSpans(body).single()
|
||||
assertEquals(expected.copy(alpha = MENTION_CHIP_ALPHA), chip.item.background)
|
||||
}
|
||||
@ -223,6 +242,8 @@ class ChatUIUtilsTest {
|
||||
message = message("no mentions in here"),
|
||||
currentUserNickname = "bob",
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
linkColor = colorScheme.secondary,
|
||||
timeFormatter = timeFormatter,
|
||||
includeTimestamp = false,
|
||||
)
|
||||
@ -236,7 +257,7 @@ class ChatUIUtilsTest {
|
||||
fun `system message uses a double-slash prefix and no brackets`() {
|
||||
val text = formatSystemMessage(
|
||||
message = message("Tor started. Routing all chats via Tor", sender = "system"),
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
timeFormatter = timeFormatter,
|
||||
).text
|
||||
|
||||
@ -249,7 +270,7 @@ class ChatUIUtilsTest {
|
||||
// messages despite being lower-priority narration.
|
||||
val annotated = formatSystemMessage(
|
||||
message = message("tor restarting", sender = "system"),
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
timeFormatter = timeFormatter,
|
||||
)
|
||||
|
||||
@ -260,7 +281,7 @@ class ChatUIUtilsTest {
|
||||
fun `system action and timestamp use their exported weights sizes and opacity`() {
|
||||
val annotated = formatSystemMessage(
|
||||
message = message("tor restarting", sender = "system"),
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
timeFormatter = timeFormatter,
|
||||
)
|
||||
|
||||
@ -273,10 +294,10 @@ class ChatUIUtilsTest {
|
||||
|
||||
assertEquals(12.sp, action.fontSize)
|
||||
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(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
|
||||
@ -327,12 +348,25 @@ class ChatUIUtilsTest {
|
||||
|
||||
// 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
|
||||
fun `peer color hue is stable across light and dark, only chroma differs`() {
|
||||
// Hue derivation must stay byte-identical to iOS; only saturation/value are tuned for
|
||||
// the redesigned neutral message body.
|
||||
val dark = colorForPeerSeed("noise:abc", isDark = true)
|
||||
val light = colorForPeerSeed("noise:abc", isDark = false)
|
||||
val seed = PeerColorSeed("noise:abc")
|
||||
val dark = colorForPeerSeed(seed, DarkBitchatPalette)
|
||||
val light = colorForPeerSeed(seed, LightBitchatPalette)
|
||||
|
||||
val darkHsv = FloatArray(3)
|
||||
val lightHsv = FloatArray(3)
|
||||
@ -342,13 +376,18 @@ class ChatUIUtilsTest {
|
||||
assertEquals(darkHsv[0].toDouble(), lightHsv[0].toDouble(), 1.0)
|
||||
assertEquals(1.0, darkHsv[1].toDouble(), 0.01)
|
||||
assertEquals(1.0, darkHsv[2].toDouble(), 0.01)
|
||||
assertEquals(0.85, lightHsv[1].toDouble(), 0.01)
|
||||
assertEquals(0.45, lightHsv[2].toDouble(), 0.01)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `peer color avoids the orange hue reserved for self`() {
|
||||
// Sweep a range of seeds; none may land within the reserved orange band.
|
||||
repeat(500) { i ->
|
||||
val color = colorForPeerSeed("noise:seed$i", isDark = true)
|
||||
val color = colorForPeerSeed(
|
||||
PeerColorSeed("noise:seed$i"),
|
||||
DarkBitchatPalette
|
||||
)
|
||||
val hsv = FloatArray(3)
|
||||
rgbToHsv(color.red, color.green, color.blue, hsv)
|
||||
val distanceFromOrange = kotlin.math.abs(hsv[0] - 30f)
|
||||
@ -360,11 +399,12 @@ class ChatUIUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `light palette is not the dark palette`() {
|
||||
assertNull(null)
|
||||
assertEquals(Color(0xFFF5F5F5), DarkBitchatPalette.textPrimary)
|
||||
assertTrue(LightBitchatPalette.textPrimary != DarkBitchatPalette.textPrimary)
|
||||
assertTrue(LightBitchatPalette.isDark != DarkBitchatPalette.isDark)
|
||||
fun `material owns standard text while Bitchat palette owns peer chroma`() {
|
||||
assertEquals(Color(0xFFF5F5F5), DarkBitchatColorScheme.onSurface)
|
||||
assertTrue(LightBitchatColorScheme.onSurface != DarkBitchatColorScheme.onSurface)
|
||||
assertTrue(
|
||||
LightBitchatPalette.peerColorValue != DarkBitchatPalette.peerColorValue
|
||||
)
|
||||
}
|
||||
|
||||
private fun rgbToHsv(r: Float, g: Float, b: Float, out: FloatArray) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user