ui: glow lock for Noise handshake instead of sync icon

Match private-chat Noise status to the Tor globe treatment: one lock
glyph with an orange pulse while handshaking, then green or red with
smooth tint cross-fades — no sync/recycle swap.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
callebtc 2026-07-27 18:13:46 +02:00
parent 6325099e2e
commit 1a390b5a1f

View File

@ -8,6 +8,7 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
@ -157,7 +158,8 @@ internal fun rememberTorConnectionVisual(normal: Color): TorConnectionVisual {
/**
* Soft, slow brightness pulse used while Tor is connecting. Keeps scale fixed so layout
* does not shift; only opacity / a faint halo breathe.
* does not shift; only opacity / a faint halo breathe. Glow strength itself cross-fades so
* starting/stopping progress never pops.
*/
@Composable
internal fun TorAwareHeaderIcon(
@ -167,7 +169,12 @@ internal fun TorAwareHeaderIcon(
contentDescription: String?,
modifier: Modifier = Modifier,
) {
val pulse = if (isProgress) {
val progressFade by animateFloatAsState(
targetValue = if (isProgress) 1f else 0f,
animationSpec = tween(BitchatMotion.EMPHASIZED_MS, easing = FastOutSlowInEasing),
label = "torGlowFade"
)
val pulse = if (progressFade > 0.01f) {
val transition = rememberInfiniteTransition(label = "torGlow")
transition.animateFloat(
initialValue = 0.42f,
@ -188,7 +195,7 @@ internal fun TorAwareHeaderIcon(
contentAlignment = Alignment.Center,
modifier = modifier.size(HeaderIconSize)
) {
if (isProgress) {
if (progressFade > 0.01f) {
val glowBrush = remember(tint) {
Brush.radialGradient(
colorStops = arrayOf(
@ -201,7 +208,7 @@ internal fun TorAwareHeaderIcon(
Box(
modifier = Modifier
.requiredSize(HeaderIconSize + 14.dp)
.graphicsLayer { alpha = pulse * 0.85f }
.graphicsLayer { alpha = pulse * 0.85f * progressFade }
.background(glowBrush)
)
}
@ -211,7 +218,9 @@ internal fun TorAwareHeaderIcon(
modifier = Modifier
.size(HeaderIconSize)
.graphicsLayer {
alpha = if (isProgress) 0.55f + pulse * 0.45f else 1f
// Idle = solid; in-progress = breathing opacity, lerped by [progressFade].
val breathing = 0.55f + pulse * 0.45f
alpha = 1f - progressFade * (1f - breathing)
},
tint = tint
)
@ -227,7 +236,12 @@ internal fun TorAwareHeaderIcon(
contentDescription: String?,
modifier: Modifier = Modifier,
) {
val pulse = if (isProgress) {
val progressFade by animateFloatAsState(
targetValue = if (isProgress) 1f else 0f,
animationSpec = tween(BitchatMotion.EMPHASIZED_MS, easing = FastOutSlowInEasing),
label = "torPainterGlowFade"
)
val pulse = if (progressFade > 0.01f) {
val transition = rememberInfiniteTransition(label = "torPainterGlow")
transition.animateFloat(
initialValue = 0.42f,
@ -246,7 +260,7 @@ internal fun TorAwareHeaderIcon(
contentAlignment = Alignment.Center,
modifier = modifier.size(HeaderIconSize)
) {
if (isProgress) {
if (progressFade > 0.01f) {
val glowBrush = remember(tint) {
Brush.radialGradient(
colorStops = arrayOf(
@ -259,7 +273,7 @@ internal fun TorAwareHeaderIcon(
Box(
modifier = Modifier
.requiredSize(HeaderIconSize + 14.dp)
.graphicsLayer { alpha = pulse * 0.85f }
.graphicsLayer { alpha = pulse * 0.85f * progressFade }
.background(glowBrush)
)
}
@ -269,13 +283,22 @@ internal fun TorAwareHeaderIcon(
modifier = Modifier
.size(HeaderIconSize)
.graphicsLayer {
alpha = if (isProgress) 0.55f + pulse * 0.45f else 1f
val breathing = 0.55f + pulse * 0.45f
alpha = 1f - progressFade * (1f - breathing)
},
tint = tint
)
}
}
/**
* Noise session status for private-chat headers.
*
* Same visual language as the main header's Tor-aware globe: one lock glyph throughout, tint
* cross-fades between states, and a soft radial glow pulse while the handshake is in flight.
* The old sync/recycle glyph is gone progress is carried by colour and motion, not by swapping
* icons.
*/
@Composable
fun NoiseSessionIcon(
sessionState: String?,
@ -283,39 +306,45 @@ fun NoiseSessionIcon(
) {
val palette = LocalBitchatPalette.current
val colorScheme = MaterialTheme.colorScheme
// The pre-redesign colours for the first two states were `0x87878700`, i.e. alpha 0x87 with
// an all-but-transparent RGB - the icons were effectively invisible. They now use the
// palette's secondary text colour.
val (iconRes, color, contentDescription) = when (sessionState) {
"uninitialized" -> Triple(
R.drawable.ic_spec_lock_open,
colorScheme.onSurfaceVariant,
stringResource(R.string.cd_ready_for_handshake)
)
"handshaking" -> Triple(
R.drawable.ic_spec_sync,
colorScheme.onSurfaceVariant,
val (targetTint, isProgress, contentDescription) = when {
sessionState == "handshaking" -> Triple(
palette.accentOrange,
true,
stringResource(R.string.cd_handshake_in_progress)
)
"established" -> Triple(
R.drawable.ic_spec_lock,
sessionState == "established" -> Triple(
colorScheme.primary,
false,
stringResource(R.string.cd_encrypted)
)
else -> { // "failed" or any other state
Triple(
R.drawable.ic_spec_warning,
colorScheme.error,
stringResource(R.string.cd_handshake_failed)
)
}
sessionState?.startsWith("failed") == true -> Triple(
colorScheme.error,
false,
stringResource(R.string.cd_handshake_failed)
)
else -> Triple(
// Not yet started — quiet grey lock, same glyph as every other state.
colorScheme.onSurfaceVariant,
false,
stringResource(R.string.cd_ready_for_handshake)
)
}
Icon(
painter = painterResource(iconRes),
// Longer than the usual chrome tint so grey → orange → green reads as a continuous wash,
// not a snap between discrete states.
val animatedTint by animateColorAsState(
targetValue = targetTint,
animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
label = "noiseSessionTint"
)
TorAwareHeaderIcon(
painter = painterResource(R.drawable.ic_spec_lock),
tint = animatedTint,
isProgress = isProgress,
contentDescription = contentDescription,
modifier = modifier,
tint = color
modifier = modifier
)
}