mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CloseButton(
|
fun CloseButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
// 44.dp to match every other tap target in the app's chrome.
|
// 44.dp to match every other tap target in the app's chrome.
|
||||||
modifier = modifier.size(44.dp),
|
modifier = modifier.size(44.dp),
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
contentColor = palette.accentGreen,
|
contentColor = colorScheme.primary,
|
||||||
containerColor = Color.Transparent
|
containerColor = Color.Transparent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -120,7 +120,6 @@ internal fun SheetIconSectionHeader(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -152,7 +151,7 @@ internal fun SheetIconSectionHeader(
|
|||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 17.sp,
|
lineHeight = 17.sp,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,11 +160,11 @@ internal fun SheetIconSectionHeader(
|
|||||||
/** Inset divider used inside grouped sheet cards (aligns with text column after the leading slot). */
|
/** Inset divider used inside grouped sheet cards (aligns with text column after the leading slot). */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SheetCardDivider() {
|
internal fun SheetCardDivider() {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(start = SheetRowDividerInset),
|
modifier = Modifier.padding(start = SheetRowDividerInset),
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
color = palette.outlineVariant
|
color = colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +211,7 @@ internal fun AboutHero(
|
|||||||
text = stringResource(R.string.about_tagline),
|
text = stringResource(R.string.about_tagline),
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
@ -238,7 +237,6 @@ internal fun AboutTabBar(
|
|||||||
onSelect: (AboutTab) -> Unit,
|
onSelect: (AboutTab) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
@ -274,7 +272,7 @@ internal fun AboutTabBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Lambda overload: the offset is animated every frame, and the non-lambda
|
// Lambda overload: the offset is animated every frame, and the non-lambda
|
||||||
@ -295,11 +293,10 @@ private fun AboutTabLabel(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
val color by animateColorAsState(
|
val color by animateColorAsState(
|
||||||
targetValue = if (isSelected) colorScheme.primary else palette.textSecondary,
|
targetValue = if (isSelected) colorScheme.primary else colorScheme.onSurfaceVariant,
|
||||||
animationSpec = tween(BitchatMotion.QUICK_MS, easing = FastOutSlowInEasing),
|
animationSpec = tween(BitchatMotion.QUICK_MS, easing = FastOutSlowInEasing),
|
||||||
label = "aboutTabLabelColor"
|
label = "aboutTabLabelColor"
|
||||||
)
|
)
|
||||||
@ -328,7 +325,6 @@ private fun AboutInstructionRow(
|
|||||||
text: String
|
text: String
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -350,7 +346,7 @@ private fun AboutInstructionRow(
|
|||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
color = palette.textPrimary
|
color = colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,7 +449,6 @@ private fun AboutFeatureRow(
|
|||||||
subtitle: String
|
subtitle: String
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -477,14 +472,14 @@ private fun AboutFeatureRow(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
color = palette.textPrimary
|
color = colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = subtitle,
|
text = subtitle,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 17.sp,
|
lineHeight = 17.sp,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,10 +493,10 @@ internal fun BitchatBadge(
|
|||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(palette.accentGreen.copy(alpha = 0.15f), RoundedCornerShape(4.dp))
|
.background(colorScheme.primary.copy(alpha = 0.15f), RoundedCornerShape(4.dp))
|
||||||
.padding(horizontal = 5.dp, vertical = 2.dp)
|
.padding(horizontal = 5.dp, vertical = 2.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@ -510,7 +505,7 @@ internal fun BitchatBadge(
|
|||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
color = palette.accentGreen
|
color = colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -556,11 +551,10 @@ internal fun SheetHeaderBadge(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(44.dp)
|
.size(44.dp)
|
||||||
.background(palette.surface, androidx.compose.foundation.shape.CircleShape),
|
.background(colorScheme.surface, androidx.compose.foundation.shape.CircleShape),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -585,8 +579,8 @@ internal fun SheetDestructiveButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isDestructive: Boolean = true
|
isDestructive: Boolean = true
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val accent = if (isDestructive) palette.accentRed else palette.accentGreen
|
val accent = if (isDestructive) colorScheme.error else colorScheme.primary
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
@ -82,17 +82,17 @@ private fun ThemeChip(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
// Cross-fade the chip so switching theme does not read as two separate flashes (the chip
|
// Cross-fade the chip so switching theme does not read as two separate flashes (the chip
|
||||||
// recolouring plus the whole app recolouring underneath it).
|
// recolouring plus the whole app recolouring underneath it).
|
||||||
val containerColor by animateColorAsState(
|
val containerColor by animateColorAsState(
|
||||||
targetValue = if (selected) palette.accentGreen else palette.surfaceVariant,
|
targetValue = if (selected) colorScheme.primary else colorScheme.surfaceVariant,
|
||||||
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
||||||
label = "themeChipContainer"
|
label = "themeChipContainer"
|
||||||
)
|
)
|
||||||
val labelColor by animateColorAsState(
|
val labelColor by animateColorAsState(
|
||||||
targetValue = if (selected) Color.White else palette.textSecondary,
|
targetValue = if (selected) Color.White else colorScheme.onSurfaceVariant,
|
||||||
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
||||||
label = "themeChipLabel"
|
label = "themeChipLabel"
|
||||||
)
|
)
|
||||||
@ -146,12 +146,12 @@ private fun SettingsToggleRow(
|
|||||||
label = "settingsRowIcon"
|
label = "settingsRowIcon"
|
||||||
)
|
)
|
||||||
val titleColor by animateColorAsState(
|
val titleColor by animateColorAsState(
|
||||||
targetValue = if (enabled) palette.textPrimary else palette.textTertiary,
|
targetValue = if (enabled) colorScheme.onSurface else palette.textTertiary,
|
||||||
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
||||||
label = "settingsRowTitle"
|
label = "settingsRowTitle"
|
||||||
)
|
)
|
||||||
val subtitleColor by animateColorAsState(
|
val subtitleColor by animateColorAsState(
|
||||||
targetValue = if (enabled) palette.textSecondary else palette.textTertiary,
|
targetValue = if (enabled) colorScheme.onSurfaceVariant else palette.textTertiary,
|
||||||
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
||||||
label = "settingsRowSubtitle"
|
label = "settingsRowSubtitle"
|
||||||
)
|
)
|
||||||
@ -213,9 +213,9 @@ private fun SettingsToggleRow(
|
|||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
colors = SwitchDefaults.colors(
|
colors = SwitchDefaults.colors(
|
||||||
checkedThumbColor = Color.White,
|
checkedThumbColor = Color.White,
|
||||||
checkedTrackColor = palette.accentGreen,
|
checkedTrackColor = colorScheme.primary,
|
||||||
uncheckedThumbColor = Color.White,
|
uncheckedThumbColor = Color.White,
|
||||||
uncheckedTrackColor = palette.surfaceVariant
|
uncheckedTrackColor = colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -310,7 +310,7 @@ fun AboutSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@ -359,7 +359,7 @@ fun AboutSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
@ -383,7 +383,7 @@ fun AboutSheet(
|
|||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(start = 54.dp),
|
modifier = Modifier.padding(start = 54.dp),
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
color = palette.outlineVariant
|
color = colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
// Proof of Work Toggle
|
// Proof of Work Toggle
|
||||||
@ -398,7 +398,7 @@ fun AboutSheet(
|
|||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(start = 54.dp),
|
modifier = Modifier.padding(start = 54.dp),
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
color = palette.outlineVariant
|
color = colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tor Toggle
|
// Tor Toggle
|
||||||
@ -417,9 +417,9 @@ fun AboutSheet(
|
|||||||
statusIndicator = if (torMode.value == TorMode.ON) {
|
statusIndicator = if (torMode.value == TorMode.ON) {
|
||||||
{
|
{
|
||||||
val statusColor = when {
|
val statusColor = when {
|
||||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> palette.accentGreen
|
torStatus.running && torStatus.bootstrapPercent >= 100 -> colorScheme.primary
|
||||||
torStatus.running -> palette.accentOrange
|
torStatus.running -> palette.accentOrange
|
||||||
else -> palette.accentRed
|
else -> colorScheme.error
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
color = statusColor,
|
color = statusColor,
|
||||||
@ -811,7 +811,7 @@ fun AboutSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -828,7 +828,7 @@ fun AboutSheet(
|
|||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = palette.textPrimary
|
color = colorScheme.onSurface
|
||||||
)
|
)
|
||||||
AnimatedCountLabel(
|
AnimatedCountLabel(
|
||||||
count = powDifficulty,
|
count = powDifficulty,
|
||||||
@ -839,7 +839,7 @@ fun AboutSheet(
|
|||||||
),
|
),
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -849,9 +849,9 @@ fun AboutSheet(
|
|||||||
valueRange = 0f..32f,
|
valueRange = 0f..32f,
|
||||||
steps = 31,
|
steps = 31,
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = palette.accentGreen,
|
thumbColor = colorScheme.primary,
|
||||||
activeTrackColor = palette.accentGreen,
|
activeTrackColor = colorScheme.primary,
|
||||||
inactiveTrackColor = palette.surfaceVariant
|
inactiveTrackColor = colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -888,7 +888,7 @@ fun AboutSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -900,9 +900,9 @@ fun AboutSheet(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
val statusColor = when {
|
val statusColor = when {
|
||||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> palette.accentGreen
|
torStatus.running && torStatus.bootstrapPercent >= 100 -> colorScheme.primary
|
||||||
torStatus.running -> palette.accentOrange
|
torStatus.running -> palette.accentOrange
|
||||||
else -> palette.accentRed
|
else -> colorScheme.error
|
||||||
}
|
}
|
||||||
Surface(color = statusColor, shape = CircleShape, modifier = Modifier.size(10.dp)) {}
|
Surface(color = statusColor, shape = CircleShape, modifier = Modifier.size(10.dp)) {}
|
||||||
Text(
|
Text(
|
||||||
@ -914,7 +914,7 @@ fun AboutSheet(
|
|||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = palette.textPrimary
|
color = colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (torStatus.lastLogLine.isNotEmpty()) {
|
if (torStatus.lastLogLine.isNotEmpty()) {
|
||||||
@ -974,7 +974,7 @@ fun AboutSheet(
|
|||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(64.dp)
|
.height(64.dp)
|
||||||
.background(palette.background.copy(alpha = topBarAlpha))
|
.background(colorScheme.background.copy(alpha = topBarAlpha))
|
||||||
) {
|
) {
|
||||||
val dismiss = LocalSheetDismiss.current
|
val dismiss = LocalSheetDismiss.current
|
||||||
CloseButton(
|
CloseButton(
|
||||||
|
|||||||
@ -119,11 +119,12 @@ internal data class TorConnectionVisual(
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun rememberTorConnectionVisual(normal: Color): TorConnectionVisual {
|
internal fun rememberTorConnectionVisual(normal: Color): TorConnectionVisual {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val torStatus by remember { ArtiTorManager.getInstance() }.statusFlow.collectAsState()
|
val torStatus by remember { ArtiTorManager.getInstance() }.statusFlow.collectAsState()
|
||||||
|
|
||||||
// ~28% of the loud accent mixed into the base tint keeps the hue without intensity.
|
// ~28% of the loud accent mixed into the base tint keeps the hue without intensity.
|
||||||
val mutedConnecting = lerp(normal, palette.accentOrange, 0.28f)
|
val mutedConnecting = lerp(normal, palette.accentOrange, 0.28f)
|
||||||
val mutedFailed = lerp(normal, palette.accentRed, 0.30f)
|
val mutedFailed = lerp(normal, colorScheme.error, 0.30f)
|
||||||
|
|
||||||
val target = when {
|
val target = when {
|
||||||
torStatus.mode == TorMode.OFF -> TorConnectionVisual(normal, isProgress = false)
|
torStatus.mode == TorMode.OFF -> TorConnectionVisual(normal, isProgress = false)
|
||||||
@ -268,18 +269,19 @@ fun NoiseSessionIcon(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
// The pre-redesign colours for the first two states were `0x87878700`, i.e. alpha 0x87 with
|
// The pre-redesign colours for the first two states were `0x87878700`, i.e. alpha 0x87 with
|
||||||
// an all-but-transparent RGB - the icons were effectively invisible. They now use the
|
// an all-but-transparent RGB - the icons were effectively invisible. They now use the
|
||||||
// palette's secondary text colour.
|
// palette's secondary text colour.
|
||||||
val (icon, color, contentDescription) = when (sessionState) {
|
val (icon, color, contentDescription) = when (sessionState) {
|
||||||
"uninitialized" -> Triple(
|
"uninitialized" -> Triple(
|
||||||
Icons.Outlined.NoEncryption,
|
Icons.Outlined.NoEncryption,
|
||||||
palette.textSecondary,
|
colorScheme.onSurfaceVariant,
|
||||||
stringResource(R.string.cd_ready_for_handshake)
|
stringResource(R.string.cd_ready_for_handshake)
|
||||||
)
|
)
|
||||||
"handshaking" -> Triple(
|
"handshaking" -> Triple(
|
||||||
Icons.Outlined.Sync,
|
Icons.Outlined.Sync,
|
||||||
palette.textSecondary,
|
colorScheme.onSurfaceVariant,
|
||||||
stringResource(R.string.cd_handshake_in_progress)
|
stringResource(R.string.cd_handshake_in_progress)
|
||||||
)
|
)
|
||||||
"established" -> Triple(
|
"established" -> Triple(
|
||||||
@ -290,7 +292,7 @@ fun NoiseSessionIcon(
|
|||||||
else -> { // "failed" or any other state
|
else -> { // "failed" or any other state
|
||||||
Triple(
|
Triple(
|
||||||
Icons.Outlined.Warning,
|
Icons.Outlined.Warning,
|
||||||
palette.accentRed,
|
colorScheme.error,
|
||||||
stringResource(R.string.cd_handshake_failed)
|
stringResource(R.string.cd_handshake_failed)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -365,19 +367,20 @@ fun PeerCounter(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
// Compute channel-aware people count and color (matches iOS logic exactly)
|
// Compute channel-aware people count and color (matches iOS logic exactly)
|
||||||
val (peopleCount, countColor) = when (selectedLocationChannel) {
|
val (peopleCount, countColor) = when (selectedLocationChannel) {
|
||||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||||
// Geohash channel: show geohash participants
|
// Geohash channel: show geohash participants
|
||||||
val count = geohashPeople.size
|
val count = geohashPeople.size
|
||||||
Pair(count, if (count > 0) palette.accentGreen else palette.textTertiary)
|
Pair(count, if (count > 0) colorScheme.primary else palette.textTertiary)
|
||||||
}
|
}
|
||||||
is com.bitchat.android.geohash.ChannelID.Mesh,
|
is com.bitchat.android.geohash.ChannelID.Mesh,
|
||||||
null -> {
|
null -> {
|
||||||
// Mesh channel: show Bluetooth-connected peers (excluding self)
|
// Mesh channel: show Bluetooth-connected peers (excluding self)
|
||||||
val count = connectedPeers.size
|
val count = connectedPeers.size
|
||||||
Pair(count, if (isConnected && count > 0) palette.accentBlue else palette.textTertiary)
|
Pair(count, if (isConnected && count > 0) colorScheme.secondary else palette.textTertiary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,7 +427,7 @@ fun PeerCounter(
|
|||||||
prefix = stringResource(R.string.channel_count_prefix),
|
prefix = stringResource(R.string.channel_count_prefix),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontSize = HeaderTextSize,
|
fontSize = HeaderTextSize,
|
||||||
color = if (isConnected) palette.accentGreen else palette.accentRed,
|
color = if (isConnected) colorScheme.primary else colorScheme.error,
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -482,7 +485,6 @@ private fun ChannelHeader(
|
|||||||
onSidebarClick: () -> Unit
|
onSidebarClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
// Back: a chevron alone is unambiguous here and buys back ~40.dp of title space that
|
// Back: a chevron alone is unambiguous here and buys back ~40.dp of title space that
|
||||||
@ -520,7 +522,7 @@ private fun ChannelHeader(
|
|||||||
text = stringResource(R.string.chat_leave),
|
text = stringResource(R.string.chat_leave),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
color = palette.accentRed,
|
color = colorScheme.error,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.clip(HeaderClusterShape)
|
.clip(HeaderClusterShape)
|
||||||
@ -667,7 +669,7 @@ private fun LocationChannelsButton(
|
|||||||
viewModel: ChatViewModel,
|
viewModel: ChatViewModel,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
// Get current channel selection from location manager
|
// Get current channel selection from location manager
|
||||||
val selectedChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
val selectedChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
@ -680,7 +682,7 @@ private fun LocationChannelsButton(
|
|||||||
// so it is plain "mesh".
|
// so it is plain "mesh".
|
||||||
else -> stringResource(R.string.mesh_label)
|
else -> stringResource(R.string.mesh_label)
|
||||||
}
|
}
|
||||||
val channelColor = if (isLocation) palette.accentGreen else palette.accentBlue
|
val channelColor = if (isLocation) colorScheme.primary else colorScheme.secondary
|
||||||
// Tor status only tints the globe (location channels). The local mesh stays blue.
|
// Tor status only tints the globe (location channels). The local mesh stays blue.
|
||||||
val torVisual = if (isLocation) {
|
val torVisual = if (isLocation) {
|
||||||
rememberTorConnectionVisual(normal = channelColor)
|
rememberTorConnectionVisual(normal = channelColor)
|
||||||
|
|||||||
@ -46,7 +46,6 @@ import com.bitchat.android.nostr.LocationNotesManager
|
|||||||
import com.bitchat.android.nostr.NearbyNotesController
|
import com.bitchat.android.nostr.NearbyNotesController
|
||||||
import com.bitchat.android.ui.media.FullScreenImageViewer
|
import com.bitchat.android.ui.media.FullScreenImageViewer
|
||||||
import com.bitchat.android.ui.theme.BitchatMotion
|
import com.bitchat.android.ui.theme.BitchatMotion
|
||||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main ChatScreen - REFACTORED to use component-based architecture
|
* Main ChatScreen - REFACTORED to use component-based architecture
|
||||||
@ -61,7 +60,6 @@ import com.bitchat.android.ui.theme.LocalBitchatPalette
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(viewModel: ChatViewModel) {
|
fun ChatScreen(viewModel: ChatViewModel) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
||||||
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||||
@ -415,17 +413,17 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
tonalElevation = 3.dp,
|
tonalElevation = 3.dp,
|
||||||
shadowElevation = 6.dp,
|
shadowElevation = 6.dp,
|
||||||
border = BorderStroke(1.dp, palette.accentGreen)
|
border = BorderStroke(1.dp, colorScheme.primary)
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) {
|
IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowDownward,
|
imageVector = Icons.Filled.ArrowDownward,
|
||||||
contentDescription = stringResource(com.bitchat.android.R.string.cd_scroll_to_bottom),
|
contentDescription = stringResource(com.bitchat.android.R.string.cd_scroll_to_bottom),
|
||||||
modifier = Modifier.size(22.dp),
|
modifier = Modifier.size(22.dp),
|
||||||
tint = palette.accentGreen
|
tint = colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -569,8 +567,6 @@ fun ChatInputSection(
|
|||||||
showMediaButtons: Boolean,
|
showMediaButtons: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
// Flat, slightly translucent screen background — the same treatment as the top bar, so the
|
// Flat, slightly translucent screen background — the same treatment as the top bar, so the
|
||||||
// two bars are visibly the same kind of surface. No gradient: a soft ramp here just looked
|
// two bars are visibly the same kind of surface. No gradient: a soft ramp here just looked
|
||||||
@ -581,7 +577,7 @@ fun ChatInputSection(
|
|||||||
.background(colorScheme.background.copy(alpha = BarBackgroundAlpha))
|
.background(colorScheme.background.copy(alpha = BarBackgroundAlpha))
|
||||||
) {
|
) {
|
||||||
// Hairline marking where chrome begins. Faint on purpose — it is a hint, not a border.
|
// Hairline marking where chrome begins. Faint on purpose — it is a hint, not a border.
|
||||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||||
|
|
||||||
// Command suggestions box
|
// Command suggestions box
|
||||||
if (showCommandSuggestions && commandSuggestions.isNotEmpty()) {
|
if (showCommandSuggestions && commandSuggestions.isNotEmpty()) {
|
||||||
@ -590,7 +586,7 @@ fun ChatInputSection(
|
|||||||
onSuggestionClick = onCommandSuggestionClick,
|
onSuggestionClick = onCommandSuggestionClick,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||||
}
|
}
|
||||||
// Retain the final populated list while the picker exits. The state layer clears
|
// Retain the final populated list while the picker exits. The state layer clears
|
||||||
// suggestions together with visibility; without this snapshot the panel would empty and
|
// suggestions together with visibility; without this snapshot the panel would empty and
|
||||||
@ -631,7 +627,7 @@ fun ChatInputSection(
|
|||||||
onSuggestionClick = onMentionSuggestionClick,
|
onSuggestionClick = onMentionSuggestionClick,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageInput(
|
MessageInput(
|
||||||
@ -682,7 +678,6 @@ private fun ChatFloatingHeader(
|
|||||||
) {
|
) {
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) }
|
val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) }
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import com.bitchat.android.model.BitchatMessage
|
|||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
import com.bitchat.android.ui.theme.BitchatPalette
|
import com.bitchat.android.ui.theme.BitchatPalette
|
||||||
import com.bitchat.android.ui.theme.ChatVisualTokens
|
import com.bitchat.android.ui.theme.ChatVisualTokens
|
||||||
|
import com.bitchat.android.ui.theme.colorForPeerSeed
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -57,7 +58,11 @@ fun formatTextMessageSender(
|
|||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
val builder = AnnotatedString.Builder()
|
val builder = AnnotatedString.Builder()
|
||||||
val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
|
val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
|
||||||
val senderColor = if (isSelf) palette.accentOrange else getPeerColor(message, palette.isDark)
|
val senderColor = if (isSelf) {
|
||||||
|
palette.accentOrange
|
||||||
|
} else {
|
||||||
|
colorForPeerSeed(peerColorSeedForMessage(message), palette)
|
||||||
|
}
|
||||||
val senderWeight = FontWeight.SemiBold
|
val senderWeight = FontWeight.SemiBold
|
||||||
val (baseName, suffix) = splitSuffix(message.sender)
|
val (baseName, suffix) = splitSuffix(message.sender)
|
||||||
|
|
||||||
@ -161,12 +166,12 @@ private fun appendBodyTimestamp(
|
|||||||
private fun appendMutedTimestamp(
|
private fun appendMutedTimestamp(
|
||||||
builder: AnnotatedString.Builder,
|
builder: AnnotatedString.Builder,
|
||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
palette: BitchatPalette,
|
contentColor: Color,
|
||||||
timeFormatter: SimpleDateFormat,
|
timeFormatter: SimpleDateFormat,
|
||||||
) {
|
) {
|
||||||
builder.pushStyle(
|
builder.pushStyle(
|
||||||
SpanStyle(
|
SpanStyle(
|
||||||
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
color = contentColor.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||||
fontSize = ChatVisualTokens.SystemTimeFontSize,
|
fontSize = ChatVisualTokens.SystemTimeFontSize,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
)
|
)
|
||||||
@ -186,6 +191,8 @@ fun formatTextMessageBody(
|
|||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
palette: BitchatPalette,
|
palette: BitchatPalette,
|
||||||
|
contentColor: Color,
|
||||||
|
linkColor: Color,
|
||||||
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
|
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
|
||||||
includeTimestamp: Boolean = true
|
includeTimestamp: Boolean = true
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
@ -196,6 +203,8 @@ fun formatTextMessageBody(
|
|||||||
content = message.content,
|
content = message.content,
|
||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = contentColor,
|
||||||
|
linkColor = linkColor,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (includeTimestamp) {
|
if (includeTimestamp) {
|
||||||
@ -212,13 +221,13 @@ fun formatTextMessageBody(
|
|||||||
*/
|
*/
|
||||||
fun formatSystemMessage(
|
fun formatSystemMessage(
|
||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
palette: BitchatPalette,
|
contentColor: Color,
|
||||||
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault())
|
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault())
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
val builder = AnnotatedString.Builder()
|
val builder = AnnotatedString.Builder()
|
||||||
builder.pushStyle(
|
builder.pushStyle(
|
||||||
SpanStyle(
|
SpanStyle(
|
||||||
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
color = contentColor.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||||
fontSize = ChatVisualTokens.SystemActionFontSize,
|
fontSize = ChatVisualTokens.SystemActionFontSize,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
@ -227,7 +236,7 @@ fun formatSystemMessage(
|
|||||||
builder.append(message.content)
|
builder.append(message.content)
|
||||||
builder.pop()
|
builder.pop()
|
||||||
|
|
||||||
appendMutedTimestamp(builder, message, palette, timeFormatter)
|
appendMutedTimestamp(builder, message, contentColor, timeFormatter)
|
||||||
return builder.toAnnotatedString()
|
return builder.toAnnotatedString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,6 +252,7 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
myPeerID: String,
|
myPeerID: String,
|
||||||
palette: BitchatPalette,
|
palette: BitchatPalette,
|
||||||
|
contentColor: Color,
|
||||||
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
|
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
|
||||||
includeSender: Boolean = true
|
includeSender: Boolean = true
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
@ -250,11 +260,15 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
|
val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
|
||||||
|
|
||||||
if (message.sender == "system") {
|
if (message.sender == "system") {
|
||||||
return formatSystemMessage(message, palette, timeFormatter)
|
return formatSystemMessage(message, contentColor, timeFormatter)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeSender) {
|
if (includeSender) {
|
||||||
val baseColor = if (isSelf) palette.accentOrange else getPeerColor(message, palette.isDark)
|
val baseColor = if (isSelf) {
|
||||||
|
palette.accentOrange
|
||||||
|
} else {
|
||||||
|
colorForPeerSeed(peerColorSeedForMessage(message), palette)
|
||||||
|
}
|
||||||
val (baseName, suffix) = splitSuffix(message.sender)
|
val (baseName, suffix) = splitSuffix(message.sender)
|
||||||
|
|
||||||
builder.pushStyle(
|
builder.pushStyle(
|
||||||
@ -291,71 +305,10 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendMutedTimestamp(builder, message, palette, timeFormatter)
|
appendMutedTimestamp(builder, message, contentColor, timeFormatter)
|
||||||
return builder.toAnnotatedString()
|
return builder.toAnnotatedString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* iOS-style peer color assignment using djb2 hash algorithm
|
|
||||||
* Avoids orange (~30°) reserved for self messages
|
|
||||||
*/
|
|
||||||
fun getPeerColor(message: BitchatMessage, isDark: Boolean): Color {
|
|
||||||
// Create seed from peer identifier (prioritizing stable keys)
|
|
||||||
val seed = when {
|
|
||||||
message.senderPeerID?.startsWith("nostr:") == true || message.senderPeerID?.startsWith("nostr_") == true -> {
|
|
||||||
// For Nostr peers, use the full key if available, otherwise the peer ID
|
|
||||||
"nostr:${message.senderPeerID.lowercase()}"
|
|
||||||
}
|
|
||||||
message.senderPeerID?.length == 16 -> {
|
|
||||||
// For ephemeral peer IDs, try to get stable Noise key, fallback to peer ID
|
|
||||||
"noise:${message.senderPeerID.lowercase()}"
|
|
||||||
}
|
|
||||||
message.senderPeerID?.length == 64 -> {
|
|
||||||
// This is already a stable Noise key
|
|
||||||
"noise:${message.senderPeerID.lowercase()}"
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Fallback to sender name
|
|
||||||
message.sender.lowercase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return colorForPeerSeed(seed, isDark)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate consistent peer color using djb2 hash.
|
|
||||||
*
|
|
||||||
* The hash and hue derivation are byte-identical to the iOS implementation, so a given peer
|
|
||||||
* resolves to the same hue on both platforms. Saturation and value are deliberately higher
|
|
||||||
* than iOS: the redesigned chat surface renders message bodies in neutral near-white, so
|
|
||||||
* nicknames need more chroma to stay distinguishable at a glance.
|
|
||||||
*/
|
|
||||||
fun colorForPeerSeed(seed: String, isDark: Boolean): Color {
|
|
||||||
// djb2 hash algorithm (matches iOS implementation)
|
|
||||||
var hash = 5381UL
|
|
||||||
for (byte in seed.toByteArray()) {
|
|
||||||
hash = ((hash shl 5) + hash) + byte.toUByte().toULong()
|
|
||||||
}
|
|
||||||
|
|
||||||
var hue = (hash % 360UL).toDouble() / 360.0
|
|
||||||
|
|
||||||
// Avoid orange (~30°) reserved for self (matches iOS logic)
|
|
||||||
val orange = 30.0 / 360.0
|
|
||||||
if (kotlin.math.abs(hue - orange) < 0.05) {
|
|
||||||
hue = (hue + 0.12) % 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
val saturation = if (isDark) 1.0 else 0.85
|
|
||||||
val brightness = if (isDark) 1.0 else 0.45
|
|
||||||
|
|
||||||
return Color.hsv(
|
|
||||||
hue = (hue * 360).toFloat(),
|
|
||||||
saturation = saturation.toFloat(),
|
|
||||||
value = brightness.toFloat()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a name into base and a '#abcd' suffix if present (matches iOS splitSuffix exactly)
|
* Split a name into base and a '#abcd' suffix if present (matches iOS splitSuffix exactly)
|
||||||
*/
|
*/
|
||||||
@ -387,7 +340,7 @@ internal fun isUnannouncedNickname(displayName: String): Boolean {
|
|||||||
* iOS-style content formatting with proper hashtag and mention handling.
|
* iOS-style content formatting with proper hashtag and mention handling.
|
||||||
*
|
*
|
||||||
* Redesign notes:
|
* Redesign notes:
|
||||||
* - Plain text renders in [BitchatPalette.textPrimary]; colour is reserved for `@mentions`,
|
* - Plain text renders in Material `onSurface`; colour is reserved for `@mentions`,
|
||||||
* links and geohashes.
|
* links and geohashes.
|
||||||
* - Mentions get a tinted background chip so they read as a distinct token inside a sentence.
|
* - Mentions get a tinted background chip so they read as a distinct token inside a sentence.
|
||||||
* The chip is tinted by *the mentioned peer's* colour, not the sender's, so `@alice` looks
|
* The chip is tinted by *the mentioned peer's* colour, not the sender's, so `@alice` looks
|
||||||
@ -401,10 +354,9 @@ private fun appendIOSFormattedContent(
|
|||||||
content: String,
|
content: String,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
palette: BitchatPalette,
|
palette: BitchatPalette,
|
||||||
|
contentColor: Color,
|
||||||
|
linkColor: Color,
|
||||||
) {
|
) {
|
||||||
val contentColor = palette.textPrimary
|
|
||||||
val linkColor = palette.accentBlue
|
|
||||||
|
|
||||||
// iOS-style patterns: allow optional '#abcd' suffix in mentions
|
// iOS-style patterns: allow optional '#abcd' suffix in mentions
|
||||||
val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex()
|
val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex()
|
||||||
val mentionPattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)".toRegex()
|
val mentionPattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)".toRegex()
|
||||||
@ -511,7 +463,10 @@ private fun appendIOSFormattedContent(
|
|||||||
palette.accentOrange
|
palette.accentOrange
|
||||||
} else {
|
} else {
|
||||||
// Tint by the *mentioned* peer so a given name looks identical everywhere.
|
// Tint by the *mentioned* peer so a given name looks identical everywhere.
|
||||||
colorForPeerSeed(mentionWithoutAt.lowercase(), palette.isDark)
|
colorForPeerSeed(
|
||||||
|
PeerColorSeed(mentionWithoutAt.lowercase(Locale.ROOT)),
|
||||||
|
palette
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val chipAlpha = if (isMentionToMe) MENTION_CHIP_ALPHA_SELF else MENTION_CHIP_ALPHA
|
val chipAlpha = if (isMentionToMe) MENTION_CHIP_ALPHA_SELF else MENTION_CHIP_ALPHA
|
||||||
val mentionWeight = if (isMentionToMe) FontWeight.Bold else FontWeight.SemiBold
|
val mentionWeight = if (isMentionToMe) FontWeight.Bold else FontWeight.SemiBold
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
|
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
@ -35,14 +36,13 @@ fun ChatUserSheet(
|
|||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
// iOS system colors (matches LocationChannelsSheet exactly)
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
val palette = LocalBitchatPalette.current
|
||||||
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) // iOS green
|
val standardGreen = colorScheme.primary
|
||||||
val standardBlue = Color(0xFF007AFF) // iOS blue
|
val standardBlue = colorScheme.secondary
|
||||||
val standardPurple = if (isDark) Color(0xFFBF5AF2) else Color(0xFFAF52DE) // iOS purple
|
val standardPurple = palette.accentPurple
|
||||||
val standardRed = Color(0xFFFF3B30) // iOS red
|
val standardRed = colorScheme.error
|
||||||
val standardGrey = if (isDark) Color(0xFF8E8E93) else Color(0xFF6D6D70) // iOS grey
|
val standardGrey = colorScheme.onSurfaceVariant
|
||||||
|
|
||||||
if (isPresented) {
|
if (isPresented) {
|
||||||
BitchatBottomSheet(
|
BitchatBottomSheet(
|
||||||
|
|||||||
@ -1195,17 +1195,12 @@ class ChatViewModel(
|
|||||||
/**
|
/**
|
||||||
* Get consistent color for a mesh peer by ID (iOS-compatible)
|
* Get consistent color for a mesh peer by ID (iOS-compatible)
|
||||||
*/
|
*/
|
||||||
fun colorForMeshPeer(peerID: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
|
fun peerColorSeedForMeshPeer(peerID: String): PeerColorSeed = meshPeerColorSeed(peerID)
|
||||||
// Try to get stable Noise key, fallback to peer ID
|
|
||||||
val seed = "noise:${peerID.lowercase()}"
|
|
||||||
return colorForPeerSeed(seed, isDark).copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get consistent color for a Nostr pubkey (iOS-compatible)
|
* Get consistent color for a Nostr pubkey (iOS-compatible)
|
||||||
*/
|
*/
|
||||||
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
|
fun peerColorSeedForNostrPubkey(pubkeyHex: String): PeerColorSeed =
|
||||||
return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark)
|
geohashViewModel.peerColorSeedForNostrPubkey(pubkeyHex)
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||||
|
import com.bitchat.android.ui.theme.colorForPeerSeed
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -45,6 +46,7 @@ fun GeohashPeopleList(
|
|||||||
val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val myHex = remember(selectedLocationChannel) {
|
val myHex = remember(selectedLocationChannel) {
|
||||||
when (val channel = selectedLocationChannel) {
|
when (val channel = selectedLocationChannel) {
|
||||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||||
@ -110,7 +112,7 @@ fun GeohashPeopleList(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding)
|
.padding(horizontal = AboutHorizontalPadding)
|
||||||
.padding(top = 10.dp),
|
.padding(top = 10.dp),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@ -212,14 +214,14 @@ private fun PeopleCard(
|
|||||||
people: List<GeoPerson>,
|
people: List<GeoPerson>,
|
||||||
row: @Composable (GeoPerson) -> Unit
|
row: @Composable (GeoPerson) -> Unit
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding)
|
.padding(horizontal = AboutHorizontalPadding)
|
||||||
.padding(top = 10.dp),
|
.padding(top = 10.dp),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
AnimatedRowColumn(items = people, key = { it.id }) { index, person ->
|
AnimatedRowColumn(items = people, key = { it.id }) { index, person ->
|
||||||
@ -250,7 +252,10 @@ private fun GeohashPersonItem(
|
|||||||
val (baseNameRaw, suffixRaw) = splitSuffix(person.displayName)
|
val (baseNameRaw, suffixRaw) = splitSuffix(person.displayName)
|
||||||
val baseName = truncateNickname(baseNameRaw)
|
val baseName = truncateNickname(baseNameRaw)
|
||||||
val suffix = if (showHashSuffix) suffixRaw else ""
|
val suffix = if (showHashSuffix) suffixRaw else ""
|
||||||
val assignedColor = viewModel.colorForNostrPubkey(person.id, palette.isDark)
|
val assignedColor = colorForPeerSeed(
|
||||||
|
viewModel.peerColorSeedForNostrPubkey(person.id),
|
||||||
|
palette
|
||||||
|
)
|
||||||
val baseColor = if (isMe) palette.accentOrange else assignedColor
|
val baseColor = if (isMe) palette.accentOrange else assignedColor
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@ -30,10 +30,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitchat.android.ui.theme.BitchatTheme
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -87,15 +87,13 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
|||||||
val initialPrecision = geohashToFocus?.length ?: 5
|
val initialPrecision = geohashToFocus?.length ?: 5
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
MaterialTheme {
|
BitchatTheme {
|
||||||
var currentGeohash by remember { mutableStateOf(geohashToFocus ?: "") }
|
var currentGeohash by remember { mutableStateOf(geohashToFocus ?: "") }
|
||||||
var precision by remember { mutableStateOf(initialPrecision.coerceIn(1, 12)) }
|
var precision by remember { mutableStateOf(initialPrecision.coerceIn(1, 12)) }
|
||||||
var webViewRef by remember { mutableStateOf<WebView?>(null) }
|
var webViewRef by remember { mutableStateOf<WebView?>(null) }
|
||||||
|
|
||||||
// iOS system-like colors used across app
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
val standardGreen = colorScheme.primary
|
||||||
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
|
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
|||||||
@ -332,10 +332,8 @@ class GeohashViewModel(
|
|||||||
fun displayNameForNostrPubkeyUI(pubkeyHex: String): String = repo.displayNameForNostrPubkeyUI(pubkeyHex)
|
fun displayNameForNostrPubkeyUI(pubkeyHex: String): String = repo.displayNameForNostrPubkeyUI(pubkeyHex)
|
||||||
fun displayNameForGeohashConversation(pubkeyHex: String, sourceGeohash: String): String = repo.displayNameForGeohashConversation(pubkeyHex, sourceGeohash)
|
fun displayNameForGeohashConversation(pubkeyHex: String, sourceGeohash: String): String = repo.displayNameForGeohashConversation(pubkeyHex, sourceGeohash)
|
||||||
|
|
||||||
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
|
fun peerColorSeedForNostrPubkey(pubkeyHex: String): PeerColorSeed =
|
||||||
val seed = "nostr:${pubkeyHex.lowercase()}"
|
nostrPeerColorSeed(pubkeyHex)
|
||||||
return colorForPeerSeed(seed, isDark).copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
|
private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
|
||||||
geoTimer?.cancel(); geoTimer = null
|
geoTimer?.cancel(); geoTimer = null
|
||||||
|
|||||||
@ -257,7 +257,8 @@ internal fun ComposerActionSurface(
|
|||||||
content: @Composable (tint: Color) -> Unit
|
content: @Composable (tint: Color) -> Unit
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
val accent = if (activeColor == Color.Unspecified) palette.accentGreen else activeColor
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val accent = if (activeColor == Color.Unspecified) colorScheme.primary else activeColor
|
||||||
|
|
||||||
val container by animateColorAsState(
|
val container by animateColorAsState(
|
||||||
// A tint rather than a fill. A solid accent disc next to the text you are typing was the
|
// A tint rather than a fill. A solid accent disc next to the text you are typing was the
|
||||||
@ -267,7 +268,7 @@ internal fun ComposerActionSurface(
|
|||||||
label = "composerButtonContainer"
|
label = "composerButtonContainer"
|
||||||
)
|
)
|
||||||
val tint by animateColorAsState(
|
val tint by animateColorAsState(
|
||||||
targetValue = if (isActive) accent else palette.textSecondary,
|
targetValue = if (isActive) accent else colorScheme.onSurfaceVariant,
|
||||||
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
||||||
label = "composerButtonTint"
|
label = "composerButtonTint"
|
||||||
)
|
)
|
||||||
@ -314,6 +315,7 @@ fun MessageInput(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val isFocused = remember { mutableStateOf(false) }
|
val isFocused = remember { mutableStateOf(false) }
|
||||||
val hasText = value.text.isNotBlank()
|
val hasText = value.text.isNotBlank()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@ -324,7 +326,7 @@ fun MessageInput(
|
|||||||
// Recording is the one state worth shouting about, so it overrides focus.
|
// Recording is the one state worth shouting about, so it overrides focus.
|
||||||
val borderColor by animateColorAsState(
|
val borderColor by animateColorAsState(
|
||||||
targetValue = when {
|
targetValue = when {
|
||||||
isRecording -> palette.accentRed.copy(alpha = 0.7f)
|
isRecording -> colorScheme.error.copy(alpha = 0.7f)
|
||||||
isFocused.value -> palette.inputOutlineFocused
|
isFocused.value -> palette.inputOutlineFocused
|
||||||
else -> palette.inputOutline
|
else -> palette.inputOutline
|
||||||
},
|
},
|
||||||
@ -371,11 +373,11 @@ fun MessageInput(
|
|||||||
// user is composing rather than reading, and green-on-black is tiring to
|
// user is composing rather than reading, and green-on-black is tiring to
|
||||||
// type into.
|
// type into.
|
||||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||||
color = palette.textPrimary,
|
color = colorScheme.onSurface,
|
||||||
fontFamily = BitchatFontFamily
|
fontFamily = BitchatFontFamily
|
||||||
),
|
),
|
||||||
cursorBrush = SolidColor(
|
cursorBrush = SolidColor(
|
||||||
if (isRecording) Color.Transparent else palette.textPrimary
|
if (isRecording) Color.Transparent else colorScheme.onSurface
|
||||||
),
|
),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
||||||
keyboardActions = KeyboardActions(onSend = {
|
keyboardActions = KeyboardActions(onSend = {
|
||||||
@ -387,8 +389,8 @@ fun MessageInput(
|
|||||||
CombinedVisualTransformation(
|
CombinedVisualTransformation(
|
||||||
listOf(
|
listOf(
|
||||||
SlashCommandVisualTransformation(
|
SlashCommandVisualTransformation(
|
||||||
commandColor = palette.accentGreen,
|
commandColor = colorScheme.primary,
|
||||||
commandBackground = palette.accentGreen.copy(alpha = 0.14f),
|
commandBackground = colorScheme.primary.copy(alpha = 0.14f),
|
||||||
),
|
),
|
||||||
MentionVisualTransformation(
|
MentionVisualTransformation(
|
||||||
mentionColor = palette.accentOrange,
|
mentionColor = palette.accentOrange,
|
||||||
@ -452,7 +454,7 @@ fun MessageInput(
|
|||||||
secs / 60, secs % 60, maxSecs / 60, maxSecs % 60
|
secs / 60, secs % 60, maxSecs / 60, maxSecs % 60
|
||||||
),
|
),
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.accentRed,
|
color = colorScheme.error,
|
||||||
fontSize = (BASE_FONT_SIZE - 4).sp
|
fontSize = (BASE_FONT_SIZE - 4).sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -595,6 +597,7 @@ private fun SendButton(
|
|||||||
enabled: Boolean = true
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
@ -602,7 +605,7 @@ private fun SendButton(
|
|||||||
isActive = enabled,
|
isActive = enabled,
|
||||||
isPressed = isPressed,
|
isPressed = isPressed,
|
||||||
// Private chats and channels keep their orange identity, disc and glyph together.
|
// Private chats and channels keep their orange identity, disc and glyph together.
|
||||||
activeColor = if (isAccented) palette.accentOrange else palette.accentGreen,
|
activeColor = if (isAccented) palette.accentOrange else colorScheme.primary,
|
||||||
modifier = modifier.clickable(
|
modifier = modifier.clickable(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = null,
|
indication = null,
|
||||||
@ -624,13 +627,13 @@ fun CommandSuggestionsBox(
|
|||||||
onSuggestionClick: (CommandSuggestion) -> Unit,
|
onSuggestionClick: (CommandSuggestion) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.background(palette.surface)
|
.background(colorScheme.surface)
|
||||||
.border(1.dp, palette.outlineVariant, RoundedCornerShape(8.dp))
|
.border(1.dp, colorScheme.outlineVariant, RoundedCornerShape(8.dp))
|
||||||
.padding(vertical = 6.dp)
|
.padding(vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
suggestions.forEach { suggestion: CommandSuggestion ->
|
suggestions.forEach { suggestion: CommandSuggestion ->
|
||||||
@ -683,7 +686,7 @@ fun CommandSuggestionItem(
|
|||||||
style = MaterialTheme.typography.bodySmall.copy(
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
fontFamily = BitchatFontFamily
|
fontFamily = BitchatFontFamily
|
||||||
),
|
),
|
||||||
color = palette.textSecondary,
|
color = colorScheme.onSurfaceVariant,
|
||||||
fontSize = (BASE_FONT_SIZE - 4).sp
|
fontSize = (BASE_FONT_SIZE - 4).sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -709,7 +712,7 @@ fun MentionSuggestionsBox(
|
|||||||
onSuggestionClick: (String) -> Unit,
|
onSuggestionClick: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -721,8 +724,8 @@ fun MentionSuggestionsBox(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.clip(MentionSuggestionsShape)
|
.clip(MentionSuggestionsShape)
|
||||||
.background(palette.surface)
|
.background(colorScheme.surface)
|
||||||
.border(1.dp, palette.outlineVariant, MentionSuggestionsShape),
|
.border(1.dp, colorScheme.outlineVariant, MentionSuggestionsShape),
|
||||||
contentPadding = PaddingValues(vertical = MentionSuggestionsVerticalPadding)
|
contentPadding = PaddingValues(vertical = MentionSuggestionsVerticalPadding)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
|
|||||||
@ -43,11 +43,8 @@ fun LinkPreviewPill(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
val textColor = colorScheme.secondary
|
||||||
|
val backgroundColor = colorScheme.secondaryContainer.copy(alpha = 0.35f)
|
||||||
// iOS-style colors
|
|
||||||
val textColor = if (isDark) Color.Green else Color(red = 0f, green = 0.5f, blue = 0f)
|
|
||||||
val backgroundColor = if (isDark) Color.Gray.copy(alpha = 0.15f) else Color.Gray.copy(alpha = 0.08f)
|
|
||||||
val borderColor = textColor.copy(alpha = 0.3f)
|
val borderColor = textColor.copy(alpha = 0.3f)
|
||||||
|
|
||||||
// Parse URL for host extraction
|
// Parse URL for host extraction
|
||||||
@ -87,7 +84,7 @@ fun LinkPreviewPill(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.size(60.dp),
|
modifier = Modifier.size(60.dp),
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
color = Color.Blue.copy(alpha = 0.1f)
|
color = colorScheme.secondaryContainer
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -97,7 +94,7 @@ fun LinkPreviewPill(
|
|||||||
imageVector = Icons.Outlined.Link,
|
imageVector = Icons.Outlined.Link,
|
||||||
contentDescription = stringResource(com.bitchat.android.R.string.cd_link),
|
contentDescription = stringResource(com.bitchat.android.R.string.cd_link),
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
tint = Color.Blue
|
tint = colorScheme.secondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,8 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
@ -40,6 +42,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@ -71,7 +74,9 @@ import com.bitchat.android.ui.theme.BitchatMotion
|
|||||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||||
import com.bitchat.android.wifiaware.WifiAwareController
|
import com.bitchat.android.wifiaware.WifiAwareController
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leading column width matching settings rows: 22.dp glyph + 16.dp gutter before title text.
|
* Leading column width matching settings rows: 22.dp glyph + 16.dp gutter before title text.
|
||||||
@ -152,8 +157,8 @@ fun LocationChannelsSheet(
|
|||||||
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
val standardGreen = palette.accentGreen
|
val standardGreen = colorScheme.primary
|
||||||
val standardBlue = palette.accentBlue
|
val standardBlue = colorScheme.secondary
|
||||||
|
|
||||||
val nearbyChannels = remember(availableChannels) {
|
val nearbyChannels = remember(availableChannels) {
|
||||||
availableChannels.filter { it.level != GeohashChannelLevel.BUILDING }
|
availableChannels.filter { it.level != GeohashChannelLevel.BUILDING }
|
||||||
@ -185,7 +190,12 @@ fun LocationChannelsSheet(
|
|||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
// Edge-to-edge + adjustResize reports the keyboard as WindowInsets.ime —
|
||||||
|
// without consuming it here the list stays full-height and the teleport
|
||||||
|
// field can't scroll above the keyboard.
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.imePadding(),
|
||||||
contentPadding = PaddingValues(top = 72.dp, bottom = 32.dp),
|
contentPadding = PaddingValues(top = 72.dp, bottom = 32.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
) {
|
) {
|
||||||
@ -203,7 +213,7 @@ fun LocationChannelsSheet(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding)
|
.padding(horizontal = AboutHorizontalPadding)
|
||||||
.padding(top = 10.dp),
|
.padding(top = 10.dp),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
ChannelOptionRow(
|
ChannelOptionRow(
|
||||||
@ -253,7 +263,7 @@ fun LocationChannelsSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
ChannelOptionRow(
|
ChannelOptionRow(
|
||||||
@ -292,7 +302,7 @@ fun LocationChannelsSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
@ -353,7 +363,7 @@ fun LocationChannelsSheet(
|
|||||||
text = stringResource(R.string.location_permission_denied),
|
text = stringResource(R.string.location_permission_denied),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.accentRed
|
color = colorScheme.error
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -389,7 +399,7 @@ fun LocationChannelsSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
@ -443,12 +453,7 @@ fun LocationChannelsSheet(
|
|||||||
customError = null
|
customError = null
|
||||||
},
|
},
|
||||||
onFocusGained = {
|
onFocusGained = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch { sheetState.expand() }
|
||||||
sheetState.expand()
|
|
||||||
listState.animateScrollToItem(
|
|
||||||
index = listState.layoutInfo.totalItemsCount - 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onOpenMap = {
|
onOpenMap = {
|
||||||
val normalized = customGeohash.trim().lowercase().replace("#", "")
|
val normalized = customGeohash.trim().lowercase().replace("#", "")
|
||||||
@ -497,7 +502,7 @@ fun LocationChannelsSheet(
|
|||||||
text = shownError,
|
text = shownError,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.accentRed,
|
color = colorScheme.error,
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
start = AboutHorizontalPadding + ChannelRowHorizontal,
|
start = AboutHorizontalPadding + ChannelRowHorizontal,
|
||||||
top = 8.dp
|
top = 8.dp
|
||||||
@ -518,7 +523,7 @@ fun LocationChannelsSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding),
|
.padding(horizontal = AboutHorizontalPadding),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
ChannelSettingsToggleRow(
|
ChannelSettingsToggleRow(
|
||||||
@ -638,7 +643,6 @@ private fun ChannelOptionRow(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
val (baseTitle, countSuffix) = splitTitleAndCount(title)
|
val (baseTitle, countSuffix) = splitTitleAndCount(title)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@ -657,7 +661,7 @@ private fun ChannelOptionRow(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(ChannelSelectedDot)
|
.size(ChannelSelectedDot)
|
||||||
.background(palette.accentGreen, CircleShape)
|
.background(colorScheme.primary, CircleShape)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
leadingIcon != null -> {
|
leadingIcon != null -> {
|
||||||
@ -686,7 +690,7 @@ private fun ChannelOptionRow(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontWeight = if (titleBold) FontWeight.SemiBold else FontWeight.Medium,
|
fontWeight = if (titleBold) FontWeight.SemiBold else FontWeight.Medium,
|
||||||
color = titleColor ?: palette.textPrimary
|
color = titleColor ?: colorScheme.onSurface
|
||||||
)
|
)
|
||||||
countSuffix?.let { count ->
|
countSuffix?.let { count ->
|
||||||
AnimatedCountLabel(
|
AnimatedCountLabel(
|
||||||
@ -694,7 +698,7 @@ private fun ChannelOptionRow(
|
|||||||
text = count,
|
text = count,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -703,7 +707,7 @@ private fun ChannelOptionRow(
|
|||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
lineHeight = 17.sp,
|
lineHeight = 17.sp,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -719,7 +723,7 @@ private fun ChannelBookmarkButton(
|
|||||||
bookmarked: Boolean,
|
bookmarked: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp)
|
.size(36.dp)
|
||||||
@ -737,7 +741,7 @@ private fun ChannelBookmarkButton(
|
|||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
if (bookmarked) R.string.cd_remove_bookmark else R.string.cd_add_bookmark
|
if (bookmarked) R.string.cd_remove_bookmark else R.string.cd_add_bookmark
|
||||||
),
|
),
|
||||||
tint = if (bookmarked) palette.accentGreen else palette.textSecondary,
|
tint = if (bookmarked) colorScheme.primary else colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -745,7 +749,7 @@ private fun ChannelBookmarkButton(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChannelLoadingRow() {
|
private fun ChannelLoadingRow() {
|
||||||
val palette = LocalBitchatPalette.current
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -760,14 +764,14 @@ private fun ChannelLoadingRow() {
|
|||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
strokeWidth = 2.dp,
|
strokeWidth = 2.dp,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.finding_nearby_channels),
|
text = stringResource(R.string.finding_nearby_channels),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.textSecondary
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -785,6 +789,10 @@ private fun CustomGeohashRow(
|
|||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val imeInsets = WindowInsets.ime
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
val normalized = customGeohash.trim().lowercase().replace("#", "")
|
val normalized = customGeohash.trim().lowercase().replace("#", "")
|
||||||
val isValid = validateGeohash(normalized)
|
val isValid = validateGeohash(normalized)
|
||||||
// Typing the last character of a valid geohash arms the button; cross-fading both the label
|
// Typing the last character of a valid geohash arms the button; cross-fading both the label
|
||||||
@ -798,7 +806,7 @@ private fun CustomGeohashRow(
|
|||||||
targetValue = if (isValid) {
|
targetValue = if (isValid) {
|
||||||
colorScheme.primary.copy(alpha = 0.16f)
|
colorScheme.primary.copy(alpha = 0.16f)
|
||||||
} else {
|
} else {
|
||||||
palette.surfaceVariant
|
colorScheme.surfaceVariant
|
||||||
},
|
},
|
||||||
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
|
||||||
label = "teleportContainer"
|
label = "teleportContainer"
|
||||||
@ -809,6 +817,7 @@ private fun CustomGeohashRow(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.bringIntoViewRequester(bringIntoViewRequester)
|
||||||
.padding(horizontal = ChannelRowHorizontal, vertical = ChannelRowVertical),
|
.padding(horizontal = ChannelRowHorizontal, vertical = ChannelRowVertical),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@ -847,7 +856,22 @@ private fun CustomGeohashRow(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.onFocusChanged { if (it.isFocused) onFocusGained() },
|
.onFocusChanged { focusState ->
|
||||||
|
if (!focusState.isFocused) return@onFocusChanged
|
||||||
|
onFocusGained()
|
||||||
|
// Wait until IME insets have landed so the LazyColumn has shrunk; bringIntoView
|
||||||
|
// against the full-height viewport leaves the field tucked under the keyboard.
|
||||||
|
// Re-request once the IME animation settles — early insets are still growing.
|
||||||
|
coroutineScope.launch {
|
||||||
|
withTimeoutOrNull(750) {
|
||||||
|
snapshotFlow { imeInsets.getBottom(density) }
|
||||||
|
.first { it > 0 }
|
||||||
|
}
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
delay(BitchatMotion.STANDARD_MS.toLong())
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
},
|
||||||
decorationBox = { inner ->
|
decorationBox = { inner ->
|
||||||
if (customGeohash.isEmpty()) {
|
if (customGeohash.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
@ -871,7 +895,7 @@ private fun CustomGeohashRow(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Map,
|
imageVector = Icons.Filled.Map,
|
||||||
contentDescription = stringResource(R.string.cd_open_map),
|
contentDescription = stringResource(R.string.cd_open_map),
|
||||||
tint = palette.textSecondary,
|
tint = colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -937,7 +961,7 @@ private fun ChannelSettingsToggleRow(
|
|||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = if (enabled) palette.textPrimary else palette.textTertiary
|
color = if (enabled) colorScheme.onSurface else palette.textTertiary
|
||||||
)
|
)
|
||||||
statusIndicator?.invoke()
|
statusIndicator?.invoke()
|
||||||
}
|
}
|
||||||
@ -946,7 +970,7 @@ private fun ChannelSettingsToggleRow(
|
|||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 17.sp,
|
lineHeight = 17.sp,
|
||||||
color = if (enabled) palette.textSecondary else palette.textTertiary
|
color = if (enabled) colorScheme.onSurfaceVariant else palette.textTertiary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
@ -956,9 +980,9 @@ private fun ChannelSettingsToggleRow(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
colors = SwitchDefaults.colors(
|
colors = SwitchDefaults.colors(
|
||||||
checkedThumbColor = Color.White,
|
checkedThumbColor = Color.White,
|
||||||
checkedTrackColor = palette.accentGreen,
|
checkedTrackColor = colorScheme.primary,
|
||||||
uncheckedThumbColor = Color.White,
|
uncheckedThumbColor = Color.White,
|
||||||
uncheckedTrackColor = palette.surfaceVariant
|
uncheckedTrackColor = colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import com.bitchat.android.R
|
|||||||
import com.bitchat.android.geohash.ChannelID
|
import com.bitchat.android.geohash.ChannelID
|
||||||
import com.bitchat.android.geohash.LocationChannelManager
|
import com.bitchat.android.geohash.LocationChannelManager
|
||||||
import com.bitchat.android.nostr.LocationNotesManager
|
import com.bitchat.android.nostr.LocationNotesManager
|
||||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Location Notes button for MainHeader.
|
* Location Notes button for MainHeader.
|
||||||
@ -35,7 +34,6 @@ fun LocationNotesButton(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
@ -54,7 +52,7 @@ fun LocationNotesButton(
|
|||||||
if (selectedLocationChannel is ChannelID.Mesh && locationEnabled) {
|
if (selectedLocationChannel is ChannelID.Mesh && locationEnabled) {
|
||||||
val hasNotes = notesCount > 0
|
val hasNotes = notesCount > 0
|
||||||
val contentDescription = stringResource(R.string.cd_location_notes)
|
val contentDescription = stringResource(R.string.cd_location_notes)
|
||||||
val normalTint = if (hasNotes) colorScheme.primary else palette.textSecondary
|
val normalTint = if (hasNotes) colorScheme.primary else colorScheme.onSurfaceVariant
|
||||||
val torVisual = rememberTorConnectionVisual(normal = normalTint)
|
val torVisual = rememberTorConnectionVisual(normal = normalTint)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import androidx.compose.material.icons.filled.ArrowUpward
|
|||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@ -51,10 +50,8 @@ fun LocationNotesSheet(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isDark = isSystemInDarkTheme()
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val accentGreen = colorScheme.primary
|
||||||
// iOS color scheme
|
|
||||||
val accentGreen = if (isDark) Color.Green else Color(0xFF008000) // dark: green, light: dark green (0, 0.5, 0)
|
|
||||||
|
|
||||||
// Managers
|
// Managers
|
||||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||||
@ -468,7 +465,6 @@ private fun LocationNotesInputSection(
|
|||||||
nickname: String?,
|
nickname: String?,
|
||||||
onSend: () -> Unit
|
onSend: () -> Unit
|
||||||
) {
|
) {
|
||||||
val isDark = isSystemInDarkTheme()
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@ -556,10 +552,8 @@ private fun LocationNotesInputSection(
|
|||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
tint = if (!sendButtonEnabled) {
|
tint = if (!sendButtonEnabled) {
|
||||||
colorScheme.onSurface.copy(alpha = 0.5f)
|
colorScheme.onSurface.copy(alpha = 0.5f)
|
||||||
} else if (isDark) {
|
|
||||||
Color.Black // Black arrow on green in dark theme
|
|
||||||
} else {
|
} else {
|
||||||
Color.White // White arrow on green in light theme
|
colorScheme.onPrimary
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import com.bitchat.android.identity.SecureIdentityStateManager
|
|||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
import com.bitchat.android.ui.theme.BitchatMotion
|
import com.bitchat.android.ui.theme.BitchatMotion
|
||||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||||
|
import com.bitchat.android.ui.theme.colorForPeerSeed
|
||||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||||
import com.bitchat.android.nostr.GeohashConversationRegistry
|
import com.bitchat.android.nostr.GeohashConversationRegistry
|
||||||
import com.bitchat.android.services.ContactDirectory
|
import com.bitchat.android.services.ContactDirectory
|
||||||
@ -132,7 +133,7 @@ fun MeshPeerListSheet(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding)
|
.padding(horizontal = AboutHorizontalPadding)
|
||||||
.padding(top = 10.dp),
|
.padding(top = 10.dp),
|
||||||
color = LocalBitchatPalette.current.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
@ -222,7 +223,7 @@ fun MeshPeerListSheet(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.QrCode,
|
imageVector = Icons.Outlined.QrCode,
|
||||||
contentDescription = stringResource(R.string.verify_title),
|
contentDescription = stringResource(R.string.verify_title),
|
||||||
tint = LocalBitchatPalette.current.textSecondary,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -265,7 +266,7 @@ private fun ChannelRow(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(SheetRowSelectedDot)
|
.size(SheetRowSelectedDot)
|
||||||
.background(palette.accentGreen, CircleShape)
|
.background(colorScheme.primary, CircleShape)
|
||||||
)
|
)
|
||||||
} else if (unreadCount > 0) {
|
} else if (unreadCount > 0) {
|
||||||
UnreadBadge(count = unreadCount, colorScheme = colorScheme)
|
UnreadBadge(count = unreadCount, colorScheme = colorScheme)
|
||||||
@ -286,7 +287,7 @@ private fun ChannelRow(
|
|||||||
text = channel,
|
text = channel,
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = if (isSelected) colorScheme.primary else palette.textPrimary,
|
color = if (isSelected) colorScheme.primary else colorScheme.onSurface,
|
||||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@ -331,7 +332,7 @@ fun PeopleSection(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = AboutHorizontalPadding)
|
.padding(horizontal = AboutHorizontalPadding)
|
||||||
.padding(top = 10.dp),
|
.padding(top = 10.dp),
|
||||||
color = palette.surface,
|
color = colorScheme.surface,
|
||||||
shape = AboutCardShape
|
shape = AboutCardShape
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
@ -588,7 +589,10 @@ private fun PeerItem(
|
|||||||
|
|
||||||
// Get consistent peer color (iOS-compatible)
|
// Get consistent peer color (iOS-compatible)
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
val assignedColor = viewModel.colorForMeshPeer(peerID, palette.isDark)
|
val assignedColor = colorForPeerSeed(
|
||||||
|
viewModel.peerColorSeedForMeshPeer(peerID),
|
||||||
|
palette
|
||||||
|
)
|
||||||
val baseColor = if (isMe) palette.accentOrange else assignedColor
|
val baseColor = if (isMe) palette.accentOrange else assignedColor
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@ -606,7 +610,7 @@ private fun PeerItem(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(SheetRowSelectedDot)
|
.size(SheetRowSelectedDot)
|
||||||
.background(palette.accentGreen, CircleShape)
|
.background(colorScheme.primary, CircleShape)
|
||||||
)
|
)
|
||||||
} else if (hasUnreadDM) {
|
} else if (hasUnreadDM) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -642,7 +646,7 @@ private fun PeerItem(
|
|||||||
else -> "Routed"
|
else -> "Routed"
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(PeerRowIconSize),
|
modifier = Modifier.size(PeerRowIconSize),
|
||||||
tint = palette.textSecondary
|
tint = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -679,7 +683,7 @@ private fun PeerItem(
|
|||||||
imageVector = Icons.Filled.Verified,
|
imageVector = Icons.Filled.Verified,
|
||||||
contentDescription = stringResource(R.string.verify_title),
|
contentDescription = stringResource(R.string.verify_title),
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = palette.accentGreen
|
tint = colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -866,7 +870,6 @@ fun PrivateChatSheet(
|
|||||||
val sheetState = rememberModalBottomSheetState(
|
val sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
)
|
)
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
if (isPresented) {
|
if (isPresented) {
|
||||||
BitchatBottomSheet(
|
BitchatBottomSheet(
|
||||||
@ -879,7 +882,7 @@ fun PrivateChatSheet(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(64.dp))
|
Spacer(modifier = Modifier.height(64.dp))
|
||||||
|
|
||||||
HorizontalDivider(thickness = 1.dp, color = palette.outlineVariant)
|
HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant)
|
||||||
|
|
||||||
// Messages list
|
// Messages list
|
||||||
var forceScrollToBottom by remember { mutableStateOf(false) }
|
var forceScrollToBottom by remember { mutableStateOf(false) }
|
||||||
|
|||||||
@ -498,6 +498,7 @@ fun MessageItem(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
myPeerID = meshService.myPeerID,
|
myPeerID = meshService.myPeerID,
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeSender = showSender
|
includeSender = showSender
|
||||||
)
|
)
|
||||||
@ -516,7 +517,7 @@ fun MessageItem(
|
|||||||
},
|
},
|
||||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.textPrimary,
|
color = colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Try to load the file packet from the path
|
// Try to load the file packet from the path
|
||||||
@ -564,14 +565,14 @@ fun MessageItem(
|
|||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.size(22.dp)
|
.size(22.dp)
|
||||||
.background(palette.surfaceVariant.copy(alpha = 0.85f), CircleShape)
|
.background(colorScheme.surfaceVariant.copy(alpha = 0.85f), CircleShape)
|
||||||
.clickable { onCancelTransfer?.invoke(message) },
|
.clickable { onCancelTransfer?.invoke(message) },
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
contentDescription = stringResource(R.string.cd_cancel),
|
contentDescription = stringResource(R.string.cd_cancel),
|
||||||
tint = palette.textPrimary,
|
tint = colorScheme.onSurface,
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -591,10 +592,10 @@ fun MessageItem(
|
|||||||
|
|
||||||
if (message.sender == "system") {
|
if (message.sender == "system") {
|
||||||
// Background narration: `// Tor started. Routing all chats…`
|
// Background narration: `// Tor started. Routing all chats…`
|
||||||
val annotatedText = remember(message, palette) {
|
val annotatedText = remember(message, colorScheme.onSurface) {
|
||||||
formatSystemMessage(
|
formatSystemMessage(
|
||||||
message = message,
|
message = message,
|
||||||
palette = palette,
|
contentColor = colorScheme.onSurface,
|
||||||
timeFormatter = timeFormatter
|
timeFormatter = timeFormatter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -614,7 +615,7 @@ fun MessageItem(
|
|||||||
softWrap = true,
|
softWrap = true,
|
||||||
overflow = TextOverflow.Visible,
|
overflow = TextOverflow.Visible,
|
||||||
style = ChatVisualTokens.SystemActionStyle.copy(
|
style = ChatVisualTokens.SystemActionStyle.copy(
|
||||||
color = palette.textPrimary.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
color = colorScheme.onSurface.copy(alpha = ChatVisualTokens.MutedTextAlpha),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -660,11 +661,20 @@ internal fun TextMessageLayout(
|
|||||||
}
|
}
|
||||||
// The timestamp trails the body rather than occupying its own column, so a short message
|
// The timestamp trails the body rather than occupying its own column, so a short message
|
||||||
// no longer reserves a full-width row for eight grey characters.
|
// no longer reserves a full-width row for eight grey characters.
|
||||||
val bodyText = remember(displayMessage, currentUserNickname, palette, timeFormatter) {
|
val bodyText = remember(
|
||||||
|
displayMessage,
|
||||||
|
currentUserNickname,
|
||||||
|
palette,
|
||||||
|
colorScheme.onSurface,
|
||||||
|
colorScheme.secondary,
|
||||||
|
timeFormatter
|
||||||
|
) {
|
||||||
formatTextMessageBody(
|
formatTextMessageBody(
|
||||||
message = displayMessage,
|
message = displayMessage,
|
||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -728,7 +738,7 @@ internal fun TextMessageLayout(
|
|||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
softWrap = true,
|
softWrap = true,
|
||||||
overflow = TextOverflow.Visible,
|
overflow = TextOverflow.Visible,
|
||||||
style = MessageBodyTextStyle.copy(color = palette.textPrimary),
|
style = MessageBodyTextStyle.copy(color = colorScheme.onSurface),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -736,7 +746,6 @@ internal fun TextMessageLayout(
|
|||||||
@Composable
|
@Composable
|
||||||
fun DeliveryStatusIcon(status: DeliveryStatus) {
|
fun DeliveryStatusIcon(status: DeliveryStatus) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val palette = LocalBitchatPalette.current
|
|
||||||
|
|
||||||
// Status advances on its own as acks come back, so a hard glyph swap reads as a flicker.
|
// Status advances on its own as acks come back, so a hard glyph swap reads as a flicker.
|
||||||
// Keyed on the status *type* rather than the instance, because Delivered/Read carry a
|
// Keyed on the status *type* rather than the instance, because Delivered/Read carry a
|
||||||
@ -758,9 +767,9 @@ fun DeliveryStatusIcon(status: DeliveryStatus) {
|
|||||||
DeliveryStatus.Delivered::class ->
|
DeliveryStatus.Delivered::class ->
|
||||||
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.8f), FontWeight.Normal)
|
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.8f), FontWeight.Normal)
|
||||||
DeliveryStatus.Read::class ->
|
DeliveryStatus.Read::class ->
|
||||||
Triple(R.string.status_delivered, palette.accentBlue, FontWeight.Bold)
|
Triple(R.string.status_delivered, colorScheme.secondary, FontWeight.Bold)
|
||||||
DeliveryStatus.Failed::class ->
|
DeliveryStatus.Failed::class ->
|
||||||
Triple(R.string.status_failed, palette.accentRed, FontWeight.Normal)
|
Triple(R.string.status_failed, colorScheme.error, FontWeight.Normal)
|
||||||
// A single subdued check, without the numeric label.
|
// A single subdued check, without the numeric label.
|
||||||
else ->
|
else ->
|
||||||
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal)
|
Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal)
|
||||||
|
|||||||
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.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -70,9 +69,9 @@ fun SecurityVerificationSheet(
|
|||||||
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||||
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
|
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val isDark = isSystemInDarkTheme()
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val accent = if (isDark) Color.Green else Color(0xFF008000)
|
val accent = colorScheme.primary
|
||||||
val boxColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.06f)
|
val boxColor = colorScheme.surfaceVariant
|
||||||
val peerHexRegex = remember { Regex("^[0-9a-fA-F]{16}$") }
|
val peerHexRegex = remember { Regex("^[0-9a-fA-F]{16}$") }
|
||||||
|
|
||||||
BitchatBottomSheet(
|
BitchatBottomSheet(
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import androidx.compose.animation.Crossfade
|
|||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -99,8 +98,7 @@ fun VerificationSheet(
|
|||||||
) {
|
) {
|
||||||
if (!isPresented) return
|
if (!isPresented) return
|
||||||
|
|
||||||
val isDark = isSystemInDarkTheme()
|
val accent = MaterialTheme.colorScheme.primary
|
||||||
val accent = if (isDark) Color.Green else Color(0xFF008000)
|
|
||||||
|
|
||||||
var selectedTab by remember { mutableStateOf(0) } // 0 = My Code, 1 = Scan
|
var selectedTab by remember { mutableStateOf(0) } // 0 = My Code, 1 = Scan
|
||||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||||
|
|||||||
@ -55,6 +55,7 @@ fun AudioMessageItem(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
myPeerID = meshService.myPeerID,
|
myPeerID = meshService.myPeerID,
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeSender = showSender
|
includeSender = showSender
|
||||||
)
|
)
|
||||||
@ -73,7 +74,7 @@ fun AudioMessageItem(
|
|||||||
},
|
},
|
||||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.textPrimary,
|
color = colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|||||||
@ -53,6 +53,7 @@ fun ImageMessageItem(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
myPeerID = meshService.myPeerID,
|
myPeerID = meshService.myPeerID,
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeSender = showSender
|
includeSender = showSender
|
||||||
)
|
)
|
||||||
@ -71,7 +72,7 @@ fun ImageMessageItem(
|
|||||||
},
|
},
|
||||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||||
fontFamily = BitchatFontFamily,
|
fontFamily = BitchatFontFamily,
|
||||||
color = palette.textPrimary,
|
color = colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@ -5,40 +5,18 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Semantic design tokens for the bitchat redesign.
|
* Bitchat-specific color tokens that do not have a faithful Material 3 semantic role.
|
||||||
*
|
*
|
||||||
* Material3's [androidx.compose.material3.ColorScheme] cannot express what this app needs:
|
* Standard backgrounds, surfaces, text, outlines, primary/secondary accents, and errors belong
|
||||||
* `onSurface` is the terminal green used for chrome, while message bodies must render in a
|
* to [androidx.compose.material3.MaterialTheme.colorScheme]. Keeping only the extra app semantics
|
||||||
* neutral near-white. Rather than repurposing M3 slots (which would repaint ~40 unrelated
|
* here lets Material components inherit correct defaults without losing Bitchat's identity.
|
||||||
* files unpredictably), the redesign exposes an explicit palette through
|
|
||||||
* [LocalBitchatPalette].
|
|
||||||
*
|
|
||||||
* Consumers should prefer these tokens over ad-hoc hex literals so accents stay consistent
|
|
||||||
* and light/dark pairs are defined in exactly one place.
|
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
data class BitchatPalette(
|
data class BitchatPalette(
|
||||||
/** Authoritative dark-mode flag. Prefer this over summing background channels. */
|
|
||||||
val isDark: Boolean,
|
|
||||||
|
|
||||||
// MARK: - Surfaces
|
|
||||||
/** Screen, header and composer background. True black in dark mode. */
|
|
||||||
val background: Color,
|
|
||||||
/** Sheets, cards and list rows: one step above [background]. */
|
|
||||||
val surface: Color,
|
|
||||||
/** Composer pill fill, chips, selected rows: one step above [surface]. */
|
|
||||||
val surfaceVariant: Color,
|
|
||||||
/** Borders, drawn at full opacity (no `.copy(alpha = ...)` needed). */
|
|
||||||
val outline: Color,
|
|
||||||
/** Hairline dividers. */
|
|
||||||
val outlineVariant: Color,
|
|
||||||
|
|
||||||
// MARK: - Form controls
|
// MARK: - Form controls
|
||||||
/**
|
/**
|
||||||
* Resting border for text inputs. Deliberately a neutral grey rather than the green-tinted
|
* Resting border for text inputs. Deliberately a neutral grey rather than the green-tinted
|
||||||
* [outline]: the composer is the one surface the user stares at while typing, and a green
|
* Material outline: the composer is the one surface the user stares at while typing.
|
||||||
* cast there made the whole bottom of the screen read as "terminal chrome" instead of a
|
|
||||||
* place to write.
|
|
||||||
*/
|
*/
|
||||||
val inputOutline: Color,
|
val inputOutline: Color,
|
||||||
/** Border for a focused text input. A step brighter, still neutral. */
|
/** Border for a focused text input. A step brighter, still neutral. */
|
||||||
@ -54,72 +32,45 @@ data class BitchatPalette(
|
|||||||
/** Resting disc behind the composer's action glyphs. Neutral grey. */
|
/** Resting disc behind the composer's action glyphs. Neutral grey. */
|
||||||
val inputButton: Color,
|
val inputButton: Color,
|
||||||
|
|
||||||
// MARK: - Text
|
// MARK: - Extra semantics
|
||||||
/** Message bodies and row titles. Neutral, not green. */
|
|
||||||
val textPrimary: Color,
|
|
||||||
/** Subtitles, secondary metadata, system/action messages. */
|
|
||||||
val textSecondary: Color,
|
|
||||||
/** Timestamps, placeholders, section labels, disabled states. */
|
/** Timestamps, placeholders, section labels, disabled states. */
|
||||||
val textTertiary: Color,
|
val textTertiary: Color,
|
||||||
|
|
||||||
// MARK: - Accents
|
|
||||||
/** Geohash channels, bookmark-on, connected/OK states. */
|
|
||||||
val accentGreen: Color,
|
|
||||||
/** Mesh channel, links, geohash references. */
|
|
||||||
val accentBlue: Color,
|
|
||||||
/** Self, mentions targeting you, unread DMs. */
|
/** Self, mentions targeting you, unread DMs. */
|
||||||
val accentOrange: Color,
|
val accentOrange: Color,
|
||||||
/** Errors, warning card, destructive actions. */
|
|
||||||
val accentRed: Color,
|
|
||||||
/** Nostr reachability. */
|
/** Nostr reachability. */
|
||||||
val accentPurple: Color,
|
val accentPurple: Color,
|
||||||
|
|
||||||
|
// MARK: - Deterministic peer colors
|
||||||
|
/** Chroma applied after deriving a peer's stable hue. */
|
||||||
|
val peerColorSaturation: Float,
|
||||||
|
/** Brightness applied after deriving a peer's stable hue. */
|
||||||
|
val peerColorValue: Float,
|
||||||
)
|
)
|
||||||
|
|
||||||
val DarkBitchatPalette = BitchatPalette(
|
val DarkBitchatPalette = BitchatPalette(
|
||||||
isDark = true,
|
|
||||||
// True black. OLED panels switch these pixels off entirely, which is both the terminal
|
|
||||||
// aesthetic the app is going for and a real battery win on a screen that is mostly
|
|
||||||
// background.
|
|
||||||
background = Color(0xFF000000),
|
|
||||||
surface = Color(0xFF0E150E),
|
|
||||||
surfaceVariant = Color(0xFF182118),
|
|
||||||
outline = Color(0xFF2A3A2A),
|
|
||||||
outlineVariant = Color(0xFF1C271C),
|
|
||||||
inputOutline = Color(0xFF333635),
|
inputOutline = Color(0xFF333635),
|
||||||
inputOutlineFocused = Color(0xFF5A605D),
|
inputOutlineFocused = Color(0xFF5A605D),
|
||||||
inputSurface = Color(0xFF0B0B0B),
|
inputSurface = Color(0xFF0B0B0B),
|
||||||
inputSurfaceFocused = Color(0xFF151515),
|
inputSurfaceFocused = Color(0xFF151515),
|
||||||
inputButton = Color(0xFF1E1E1E),
|
inputButton = Color(0xFF1E1E1E),
|
||||||
textPrimary = Color(0xFFF5F5F5),
|
|
||||||
textSecondary = Color(0xFF9AA69A),
|
|
||||||
textTertiary = Color(0xFF6B776B),
|
textTertiary = Color(0xFF6B776B),
|
||||||
accentGreen = Color(0xFF32D74B),
|
|
||||||
accentBlue = Color(0xFF0A84FF),
|
|
||||||
accentOrange = Color(0xFFFF9F0A),
|
accentOrange = Color(0xFFFF9F0A),
|
||||||
accentRed = Color(0xFFFF453A),
|
|
||||||
accentPurple = Color(0xFFBF5AF2),
|
accentPurple = Color(0xFFBF5AF2),
|
||||||
|
peerColorSaturation = 1f,
|
||||||
|
peerColorValue = 1f,
|
||||||
)
|
)
|
||||||
|
|
||||||
val LightBitchatPalette = BitchatPalette(
|
val LightBitchatPalette = BitchatPalette(
|
||||||
isDark = false,
|
|
||||||
background = Color(0xFFFFFFFF),
|
|
||||||
surface = Color(0xFFF2F6F2),
|
|
||||||
surfaceVariant = Color(0xFFE7EDE7),
|
|
||||||
outline = Color(0xFFCBD6CB),
|
|
||||||
outlineVariant = Color(0xFFDEE6DE),
|
|
||||||
inputOutline = Color(0xFFCFD3D1),
|
inputOutline = Color(0xFFCFD3D1),
|
||||||
inputOutlineFocused = Color(0xFF8E9490),
|
inputOutlineFocused = Color(0xFF8E9490),
|
||||||
inputSurface = Color(0xFFFAFAFA),
|
inputSurface = Color(0xFFFAFAFA),
|
||||||
inputSurfaceFocused = Color(0xFFF2F2F2),
|
inputSurfaceFocused = Color(0xFFF2F2F2),
|
||||||
inputButton = Color(0xFFE8E8E8),
|
inputButton = Color(0xFFE8E8E8),
|
||||||
textPrimary = Color(0xFF131A13),
|
|
||||||
textSecondary = Color(0xFF4C574C),
|
|
||||||
textTertiary = Color(0xFF757F75),
|
textTertiary = Color(0xFF757F75),
|
||||||
accentGreen = Color(0xFF248A3D),
|
|
||||||
accentBlue = Color(0xFF007AFF),
|
|
||||||
accentOrange = Color(0xFFFF9500),
|
accentOrange = Color(0xFFFF9500),
|
||||||
accentRed = Color(0xFFD70015),
|
|
||||||
accentPurple = Color(0xFFAF52DE),
|
accentPurple = Color(0xFFAF52DE),
|
||||||
|
peerColorSaturation = 0.85f,
|
||||||
|
peerColorValue = 0.45f,
|
||||||
)
|
)
|
||||||
|
|
||||||
val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette }
|
val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette }
|
||||||
|
|||||||
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.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
|
||||||
// Colors that match the iOS bitchat theme.
|
// Standard UI semantics live in Material so stock components and custom Bitchat composables
|
||||||
// Surface/outline/error slots are derived from BitchatPalette so that screens which have not
|
// share one source of truth. LocalBitchatPalette below only supplies app-specific extra colors.
|
||||||
// yet migrated to the explicit palette still pick up the redesigned tinted surfaces.
|
internal val DarkBitchatColorScheme = darkColorScheme(
|
||||||
private val DarkColorScheme = darkColorScheme(
|
primary = Color(0xFF32D74B),
|
||||||
primary = Color(0xFF39FF14), // Bright green (terminal-like)
|
|
||||||
onPrimary = Color.Black,
|
onPrimary = Color.Black,
|
||||||
secondary = Color(0xFF2ECB10), // Darker green
|
primaryContainer = Color(0xFF163D1D),
|
||||||
|
onPrimaryContainer = Color(0xFFB8F5C1),
|
||||||
|
secondary = Color(0xFF0A84FF),
|
||||||
onSecondary = Color.Black,
|
onSecondary = Color.Black,
|
||||||
background = DarkBitchatPalette.background,
|
secondaryContainer = Color(0xFF082E54),
|
||||||
onBackground = Color(0xFF39FF14), // Green on near-black
|
onSecondaryContainer = Color(0xFFC2E0FF),
|
||||||
surface = DarkBitchatPalette.surface,
|
tertiary = DarkBitchatPalette.accentOrange,
|
||||||
onSurface = Color(0xFF39FF14), // Green text
|
onTertiary = Color.Black,
|
||||||
surfaceVariant = DarkBitchatPalette.surfaceVariant,
|
background = Color(0xFF000000),
|
||||||
onSurfaceVariant = DarkBitchatPalette.textSecondary,
|
onBackground = Color(0xFFF5F5F5),
|
||||||
outline = DarkBitchatPalette.outline,
|
surface = Color(0xFF0E150E),
|
||||||
outlineVariant = DarkBitchatPalette.outlineVariant,
|
onSurface = Color(0xFFF5F5F5),
|
||||||
error = DarkBitchatPalette.accentRed,
|
surfaceVariant = Color(0xFF182118),
|
||||||
|
onSurfaceVariant = Color(0xFF9AA69A),
|
||||||
|
outline = Color(0xFF2A3A2A),
|
||||||
|
outlineVariant = Color(0xFF1C271C),
|
||||||
|
error = Color(0xFFFF453A),
|
||||||
onError = Color.Black
|
onError = Color.Black
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
internal val LightBitchatColorScheme = lightColorScheme(
|
||||||
primary = Color(0xFF008000), // Dark green
|
primary = Color(0xFF248A3D),
|
||||||
onPrimary = Color.White,
|
onPrimary = Color.White,
|
||||||
secondary = Color(0xFF006600), // Even darker green
|
primaryContainer = Color(0xFFD5F1D8),
|
||||||
|
onPrimaryContainer = Color(0xFF0A3212),
|
||||||
|
secondary = Color(0xFF007AFF),
|
||||||
onSecondary = Color.White,
|
onSecondary = Color.White,
|
||||||
background = LightBitchatPalette.background,
|
secondaryContainer = Color(0xFFD6E9FF),
|
||||||
onBackground = Color(0xFF008000), // Dark green on off-white
|
onSecondaryContainer = Color(0xFF002C5C),
|
||||||
surface = LightBitchatPalette.surface,
|
tertiary = LightBitchatPalette.accentOrange,
|
||||||
onSurface = Color(0xFF008000), // Dark green text
|
onTertiary = Color.Black,
|
||||||
surfaceVariant = LightBitchatPalette.surfaceVariant,
|
background = Color(0xFFFFFFFF),
|
||||||
onSurfaceVariant = LightBitchatPalette.textSecondary,
|
onBackground = Color(0xFF131A13),
|
||||||
outline = LightBitchatPalette.outline,
|
surface = Color(0xFFF2F6F2),
|
||||||
outlineVariant = LightBitchatPalette.outlineVariant,
|
onSurface = Color(0xFF131A13),
|
||||||
error = LightBitchatPalette.accentRed,
|
surfaceVariant = Color(0xFFE7EDE7),
|
||||||
|
onSurfaceVariant = Color(0xFF4C574C),
|
||||||
|
outline = Color(0xFFCBD6CB),
|
||||||
|
outlineVariant = Color(0xFFDEE6DE),
|
||||||
|
error = Color(0xFFD70015),
|
||||||
onError = Color.White
|
onError = Color.White
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,7 +82,7 @@ fun BitchatTheme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val colorScheme = if (shouldUseDark) DarkColorScheme else LightColorScheme
|
val colorScheme = if (shouldUseDark) DarkBitchatColorScheme else LightBitchatColorScheme
|
||||||
val palette = if (shouldUseDark) DarkBitchatPalette else LightBitchatPalette
|
val palette = if (shouldUseDark) DarkBitchatPalette else LightBitchatPalette
|
||||||
|
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|||||||
@ -7,16 +7,18 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||||
import com.bitchat.android.ui.theme.ChatVisualTokens
|
import com.bitchat.android.ui.theme.ChatVisualTokens
|
||||||
|
import com.bitchat.android.ui.theme.DarkBitchatColorScheme
|
||||||
import com.bitchat.android.ui.theme.DarkBitchatPalette
|
import com.bitchat.android.ui.theme.DarkBitchatPalette
|
||||||
|
import com.bitchat.android.ui.theme.LightBitchatColorScheme
|
||||||
import com.bitchat.android.ui.theme.LightBitchatPalette
|
import com.bitchat.android.ui.theme.LightBitchatPalette
|
||||||
import com.bitchat.android.ui.theme.MessageBodyTextStyle
|
import com.bitchat.android.ui.theme.MessageBodyTextStyle
|
||||||
import com.bitchat.android.ui.theme.MessageSenderTextStyle
|
import com.bitchat.android.ui.theme.MessageSenderTextStyle
|
||||||
|
import com.bitchat.android.ui.theme.colorForPeerSeed
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@ -33,6 +35,7 @@ class ChatUIUtilsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val palette = DarkBitchatPalette
|
private val palette = DarkBitchatPalette
|
||||||
|
private val colorScheme = DarkBitchatColorScheme
|
||||||
|
|
||||||
private fun message(
|
private fun message(
|
||||||
content: String,
|
content: String,
|
||||||
@ -71,6 +74,8 @@ class ChatUIUtilsTest {
|
|||||||
message = message("hello there"),
|
message = message("hello there"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,6 +94,8 @@ class ChatUIUtilsTest {
|
|||||||
message = message("mined", powDifficulty = 8),
|
message = message("mined", powDifficulty = 8),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -106,6 +113,8 @@ class ChatUIUtilsTest {
|
|||||||
message = message("hello there"),
|
message = message("hello there"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeTimestamp = false,
|
includeTimestamp = false,
|
||||||
)
|
)
|
||||||
@ -119,12 +128,14 @@ class ChatUIUtilsTest {
|
|||||||
message = message("plain words"),
|
message = message("plain words"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeTimestamp = false,
|
includeTimestamp = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val textStyle = body.spanStyles.first { it.start == 0 }
|
val textStyle = body.spanStyles.first { it.start == 0 }
|
||||||
assertEquals(palette.textPrimary, textStyle.item.color)
|
assertEquals(colorScheme.onSurface, textStyle.item.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -146,6 +157,8 @@ class ChatUIUtilsTest {
|
|||||||
message = message("my own words", sender = "bob"),
|
message = message("my own words", sender = "bob"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeTimestamp = false,
|
includeTimestamp = false,
|
||||||
)
|
)
|
||||||
@ -170,6 +183,8 @@ class ChatUIUtilsTest {
|
|||||||
message = message("hey @carol#04af what's up"),
|
message = message("hey @carol#04af what's up"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeTimestamp = false,
|
includeTimestamp = false,
|
||||||
)
|
)
|
||||||
@ -188,6 +203,8 @@ class ChatUIUtilsTest {
|
|||||||
message = message("ping @bob now"),
|
message = message("ping @bob now"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeTimestamp = false,
|
includeTimestamp = false,
|
||||||
)
|
)
|
||||||
@ -208,11 +225,13 @@ class ChatUIUtilsTest {
|
|||||||
message = message("cc @carol#04af"),
|
message = message("cc @carol#04af"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeTimestamp = false,
|
includeTimestamp = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val expected = colorForPeerSeed("carol#04af", palette.isDark)
|
val expected = colorForPeerSeed(PeerColorSeed("carol#04af"), palette)
|
||||||
val chip = mentionChipSpans(body).single()
|
val chip = mentionChipSpans(body).single()
|
||||||
assertEquals(expected.copy(alpha = MENTION_CHIP_ALPHA), chip.item.background)
|
assertEquals(expected.copy(alpha = MENTION_CHIP_ALPHA), chip.item.background)
|
||||||
}
|
}
|
||||||
@ -223,6 +242,8 @@ class ChatUIUtilsTest {
|
|||||||
message = message("no mentions in here"),
|
message = message("no mentions in here"),
|
||||||
currentUserNickname = "bob",
|
currentUserNickname = "bob",
|
||||||
palette = palette,
|
palette = palette,
|
||||||
|
contentColor = colorScheme.onSurface,
|
||||||
|
linkColor = colorScheme.secondary,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
includeTimestamp = false,
|
includeTimestamp = false,
|
||||||
)
|
)
|
||||||
@ -236,7 +257,7 @@ class ChatUIUtilsTest {
|
|||||||
fun `system message uses a double-slash prefix and no brackets`() {
|
fun `system message uses a double-slash prefix and no brackets`() {
|
||||||
val text = formatSystemMessage(
|
val text = formatSystemMessage(
|
||||||
message = message("Tor started. Routing all chats via Tor", sender = "system"),
|
message = message("Tor started. Routing all chats via Tor", sender = "system"),
|
||||||
palette = palette,
|
contentColor = colorScheme.onSurface,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
).text
|
).text
|
||||||
|
|
||||||
@ -249,7 +270,7 @@ class ChatUIUtilsTest {
|
|||||||
// messages despite being lower-priority narration.
|
// messages despite being lower-priority narration.
|
||||||
val annotated = formatSystemMessage(
|
val annotated = formatSystemMessage(
|
||||||
message = message("tor restarting", sender = "system"),
|
message = message("tor restarting", sender = "system"),
|
||||||
palette = palette,
|
contentColor = colorScheme.onSurface,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -260,7 +281,7 @@ class ChatUIUtilsTest {
|
|||||||
fun `system action and timestamp use their exported weights sizes and opacity`() {
|
fun `system action and timestamp use their exported weights sizes and opacity`() {
|
||||||
val annotated = formatSystemMessage(
|
val annotated = formatSystemMessage(
|
||||||
message = message("tor restarting", sender = "system"),
|
message = message("tor restarting", sender = "system"),
|
||||||
palette = palette,
|
contentColor = colorScheme.onSurface,
|
||||||
timeFormatter = timeFormatter,
|
timeFormatter = timeFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -273,10 +294,10 @@ class ChatUIUtilsTest {
|
|||||||
|
|
||||||
assertEquals(12.sp, action.fontSize)
|
assertEquals(12.sp, action.fontSize)
|
||||||
assertEquals(FontWeight.Medium, action.fontWeight)
|
assertEquals(FontWeight.Medium, action.fontWeight)
|
||||||
assertEquals(palette.textPrimary.copy(alpha = 0.5f), action.color)
|
assertEquals(colorScheme.onSurface.copy(alpha = 0.5f), action.color)
|
||||||
assertEquals(10.sp, time.fontSize)
|
assertEquals(10.sp, time.fontSize)
|
||||||
assertEquals(FontWeight.Normal, time.fontWeight)
|
assertEquals(FontWeight.Normal, time.fontWeight)
|
||||||
assertEquals(palette.textPrimary.copy(alpha = 0.5f), time.color)
|
assertEquals(colorScheme.onSurface.copy(alpha = 0.5f), time.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Sender label
|
// MARK: - Sender label
|
||||||
@ -327,12 +348,25 @@ class ChatUIUtilsTest {
|
|||||||
|
|
||||||
// MARK: - Peer colors
|
// MARK: - Peer colors
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `peer seed factories normalize identities without resolving UI colors`() {
|
||||||
|
assertEquals(
|
||||||
|
PeerColorSeed("noise:abcdef"),
|
||||||
|
meshPeerColorSeed("ABCDEF")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
PeerColorSeed("nostr:abcdef"),
|
||||||
|
nostrPeerColorSeed("ABCDEF")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `peer color hue is stable across light and dark, only chroma differs`() {
|
fun `peer color hue is stable across light and dark, only chroma differs`() {
|
||||||
// Hue derivation must stay byte-identical to iOS; only saturation/value are tuned for
|
// Hue derivation must stay byte-identical to iOS; only saturation/value are tuned for
|
||||||
// the redesigned neutral message body.
|
// the redesigned neutral message body.
|
||||||
val dark = colorForPeerSeed("noise:abc", isDark = true)
|
val seed = PeerColorSeed("noise:abc")
|
||||||
val light = colorForPeerSeed("noise:abc", isDark = false)
|
val dark = colorForPeerSeed(seed, DarkBitchatPalette)
|
||||||
|
val light = colorForPeerSeed(seed, LightBitchatPalette)
|
||||||
|
|
||||||
val darkHsv = FloatArray(3)
|
val darkHsv = FloatArray(3)
|
||||||
val lightHsv = FloatArray(3)
|
val lightHsv = FloatArray(3)
|
||||||
@ -342,13 +376,18 @@ class ChatUIUtilsTest {
|
|||||||
assertEquals(darkHsv[0].toDouble(), lightHsv[0].toDouble(), 1.0)
|
assertEquals(darkHsv[0].toDouble(), lightHsv[0].toDouble(), 1.0)
|
||||||
assertEquals(1.0, darkHsv[1].toDouble(), 0.01)
|
assertEquals(1.0, darkHsv[1].toDouble(), 0.01)
|
||||||
assertEquals(1.0, darkHsv[2].toDouble(), 0.01)
|
assertEquals(1.0, darkHsv[2].toDouble(), 0.01)
|
||||||
|
assertEquals(0.85, lightHsv[1].toDouble(), 0.01)
|
||||||
|
assertEquals(0.45, lightHsv[2].toDouble(), 0.01)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `peer color avoids the orange hue reserved for self`() {
|
fun `peer color avoids the orange hue reserved for self`() {
|
||||||
// Sweep a range of seeds; none may land within the reserved orange band.
|
// Sweep a range of seeds; none may land within the reserved orange band.
|
||||||
repeat(500) { i ->
|
repeat(500) { i ->
|
||||||
val color = colorForPeerSeed("noise:seed$i", isDark = true)
|
val color = colorForPeerSeed(
|
||||||
|
PeerColorSeed("noise:seed$i"),
|
||||||
|
DarkBitchatPalette
|
||||||
|
)
|
||||||
val hsv = FloatArray(3)
|
val hsv = FloatArray(3)
|
||||||
rgbToHsv(color.red, color.green, color.blue, hsv)
|
rgbToHsv(color.red, color.green, color.blue, hsv)
|
||||||
val distanceFromOrange = kotlin.math.abs(hsv[0] - 30f)
|
val distanceFromOrange = kotlin.math.abs(hsv[0] - 30f)
|
||||||
@ -360,11 +399,12 @@ class ChatUIUtilsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `light palette is not the dark palette`() {
|
fun `material owns standard text while Bitchat palette owns peer chroma`() {
|
||||||
assertNull(null)
|
assertEquals(Color(0xFFF5F5F5), DarkBitchatColorScheme.onSurface)
|
||||||
assertEquals(Color(0xFFF5F5F5), DarkBitchatPalette.textPrimary)
|
assertTrue(LightBitchatColorScheme.onSurface != DarkBitchatColorScheme.onSurface)
|
||||||
assertTrue(LightBitchatPalette.textPrimary != DarkBitchatPalette.textPrimary)
|
assertTrue(
|
||||||
assertTrue(LightBitchatPalette.isDark != DarkBitchatPalette.isDark)
|
LightBitchatPalette.peerColorValue != DarkBitchatPalette.peerColorValue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rgbToHsv(r: Float, g: Float, b: Float, out: FloatArray) {
|
private fun rgbToHsv(r: Float, g: Float, b: Float, out: FloatArray) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user