Compare commits

..

2 Commits

Author SHA1 Message Date
7630aa6874 fix: LOCKED UI как в Telegram — CANCEL текст вместо ✕, без blob при lock
Telegram LOCKED layout: [timer] [waveform] [CANCEL] [⏸] [Send]

Изменения:
- RecordLockedControls: убрана круглая ✕ кнопка delete
- Вместо неё: текст "CANCEL" синим bold 15sp (как в Telegram)
- Пауза иконка увеличена 12→14dp, фон 15% alpha
- Blob анимация скрыта при LOCKED/PAUSED (Telegram: solid circle)
- Spacing 8→12dp между CANCEL и паузой

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:15:09 +05:00
afebbf6acb fix: slide-to-cancel не работает при LOCKED/PAUSED — как в Telegram
Telegram: при sendButtonVisible=true gesture handler возвращает false,
полностью блокируя горизонтальный свайп. Slide-to-cancel исчезает,
вместо него кнопка Cancel.

Изменения:
- Gesture handler: только RECORDING обрабатывает slide (было RECORDING||LOCKED)
- slideDx/slideDy не обновляются при LOCKED/PAUSED
- При lock: slideDx=0, slideDy=0 — сбрасываем горизонтальное смещение
- AnimatedContent уже переключает SlideToCancel→waveform при LOCKED

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:08:33 +05:00

View File

@@ -781,6 +781,17 @@ private fun VoiceWaveformBar(
} }
} }
/**
* Telegram-exact locked recording controls.
*
* Layout: [CANCEL text-button] [⏸/▶ circle button]
*
* - CANCEL = blue text (15sp bold, uppercase), clickable — cancels recording
* - ⏸ = small circle button (36dp), toggles pause/resume
* - No separate delete icon — CANCEL IS delete
*
* Reference: ChatActivityEnterView recordedAudioPanel + SlideTextView cancelToProgress
*/
@Composable @Composable
private fun RecordLockedControls( private fun RecordLockedControls(
isPaused: Boolean, isPaused: Boolean,
@@ -789,37 +800,31 @@ private fun RecordLockedControls(
onTogglePause: () -> Unit, onTogglePause: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val deleteBgColor = if (isDarkTheme) Color(0xFF444444) else Color(0xFFE0E0E0) val cancelColor = if (isDarkTheme) Color(0xFF69CCFF) else Color(0xFF2D9CFF)
val deleteIconColor = if (isDarkTheme) Color.White.copy(alpha = 0.8f) else Color(0xFF666666) val pauseBgColor = if (isDarkTheme) Color(0xFF69CCFF).copy(alpha = 0.15f) else Color(0xFF2D9CFF).copy(alpha = 0.1f)
val pauseBgColor = if (isDarkTheme) Color(0xFF69CCFF).copy(alpha = 0.3f) else Color(0xFF2D9CFF).copy(alpha = 0.2f)
val pauseIconColor = if (isDarkTheme) Color(0xFF69CCFF) else Color(0xFF2D9CFF) val pauseIconColor = if (isDarkTheme) Color(0xFF69CCFF) else Color(0xFF2D9CFF)
Row( Row(
modifier = modifier, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
// Delete button // CANCEL text button — Telegram: blue bold uppercase
Box( Text(
text = "CANCEL",
color = cancelColor,
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
modifier = Modifier modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(deleteBgColor)
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = null indication = null
) { onDelete() }, ) { onDelete() }
contentAlignment = Alignment.Center .padding(horizontal = 4.dp, vertical = 8.dp)
) { )
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Delete recording",
tint = deleteIconColor,
modifier = Modifier.size(18.dp)
)
}
// Pause/Resume button // Pause/Resume button — circle with icon
Box( Box(
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
@@ -832,7 +837,8 @@ private fun RecordLockedControls(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (isPaused) { if (isPaused) {
Canvas(modifier = Modifier.size(12.dp)) { // Play triangle
Canvas(modifier = Modifier.size(14.dp)) {
val path = Path().apply { val path = Path().apply {
moveTo(size.width * 0.2f, 0f) moveTo(size.width * 0.2f, 0f)
lineTo(size.width, size.height / 2f) lineTo(size.width, size.height / 2f)
@@ -842,19 +848,20 @@ private fun RecordLockedControls(
drawPath(path, color = pauseIconColor) drawPath(path, color = pauseIconColor)
} }
} else { } else {
Canvas(modifier = Modifier.size(12.dp)) { // Pause bars
val barW = size.width * 0.25f Canvas(modifier = Modifier.size(14.dp)) {
val gap = size.width * 0.15f val barW = size.width * 0.22f
val gap = size.width * 0.14f
drawRoundRect( drawRoundRect(
color = pauseIconColor, color = pauseIconColor,
topLeft = Offset(size.width / 2f - gap - barW, 0f), topLeft = Offset(size.width / 2f - gap - barW, size.height * 0.1f),
size = androidx.compose.ui.geometry.Size(barW, size.height), size = androidx.compose.ui.geometry.Size(barW, size.height * 0.8f),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(barW / 3f) cornerRadius = androidx.compose.ui.geometry.CornerRadius(barW / 3f)
) )
drawRoundRect( drawRoundRect(
color = pauseIconColor, color = pauseIconColor,
topLeft = Offset(size.width / 2f + gap, 0f), topLeft = Offset(size.width / 2f + gap, size.height * 0.1f),
size = androidx.compose.ui.geometry.Size(barW, size.height), size = androidx.compose.ui.geometry.Size(barW, size.height * 0.8f),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(barW / 3f) cornerRadius = androidx.compose.ui.geometry.CornerRadius(barW / 3f)
) )
} }
@@ -2240,18 +2247,20 @@ fun MessageInputBar(
} }
} }
// Blob: 48dp base → 1.7x = ~82dp visual (matches Telegram circleRadius 41dp) // Blob: only during RECORDING (Telegram hides waves when locked)
VoiceButtonBlob( if (recordUiState == RecordUiState.RECORDING) {
voiceLevel = voiceLevel, VoiceButtonBlob(
isDarkTheme = isDarkTheme, voiceLevel = voiceLevel,
modifier = Modifier isDarkTheme = isDarkTheme,
.size(48.dp) modifier = Modifier
.graphicsLayer { .size(48.dp)
scaleX = 1.7f .graphicsLayer {
scaleY = 1.7f scaleX = 1.7f
clip = false scaleY = 1.7f
} clip = false
) }
)
}
// Solid circle: 48dp layout, scaled to 82dp visual // Solid circle: 48dp layout, scaled to 82dp visual
val sendScale by animateFloatAsState( val sendScale by animateFloatAsState(
@@ -2513,33 +2522,32 @@ fun MessageInputBar(
val absDy = kotlin.math.abs(dy) val absDy = kotlin.math.abs(dy)
if (absDx > maxAbsDx) maxAbsDx = absDx if (absDx > maxAbsDx) maxAbsDx = absDx
if (absDy > maxAbsDy) maxAbsDy = absDy if (absDy > maxAbsDy) maxAbsDy = absDy
} else if ( } else if (recordUiState == RecordUiState.RECORDING) {
recordUiState == RecordUiState.RECORDING || // Only RECORDING processes slide gestures
recordUiState == RecordUiState.LOCKED // LOCKED/PAUSED: no gesture processing (Telegram: return false)
) {
val dx = change.position.x - pressStartX val dx = change.position.x - pressStartX
val dy = change.position.y - pressStartY val dy = change.position.y - pressStartY
slideDx = dx slideDx = dx
slideDy = dy slideDy = dy
lockProgress = ((-dy) / lockDragThresholdPx).coerceIn(0f, 1f)
if (recordUiState == RecordUiState.RECORDING) { if (dx <= -cancelDragThresholdPx) {
lockProgress = ((-dy) / lockDragThresholdPx).coerceIn(0f, 1f) inputJumpLog(
if (dx <= -cancelDragThresholdPx) { "gesture CANCEL dx=${dx.toInt()} threshold=${cancelDragThresholdPx.toInt()} mode=$recordMode"
inputJumpLog( )
"gesture CANCEL dx=${dx.toInt()} threshold=${cancelDragThresholdPx.toInt()} mode=$recordMode" stopVoiceRecording(send = false)
) setRecordUiState(RecordUiState.IDLE, "slide-cancel")
stopVoiceRecording(send = false) resetGestureState()
setRecordUiState(RecordUiState.IDLE, "slide-cancel") finished = true
resetGestureState() } else if (dy <= -lockDragThresholdPx) {
finished = true view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP)
} else if (dy <= -lockDragThresholdPx) { lockProgress = 1f
view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP) slideDx = 0f // reset horizontal slide on lock
lockProgress = 1f slideDy = 0f
setRecordUiState( setRecordUiState(
RecordUiState.LOCKED, RecordUiState.LOCKED,
"slide-lock dy=${dy.toInt()}" "slide-lock dy=${dy.toInt()}"
) )
}
} }
} }
change.consume() change.consume()