fix: LOCKED panel 1:1 с Telegram — полностью другой layout при lock

Telegram при LOCKED: таймер и dot СКРЫТЫ, вместо них:
- [Delete 44dp] — красная иконка удаления слева
- [Waveform] — заполняет оставшееся место
- Lock→Pause кнопка наверху (отдельный overlay)
- Circle = Send (без blob)

При RECORDING (без изменений):
- [dot][timer] [◀ Slide to cancel] [Circle+Blob]

Реализация: AnimatedContent crossfade между двумя полностью
разными panel layouts. RecordLockedControls больше не используется
в панели — delete в самой панели, pause в LockIcon overlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 00:33:26 +05:00
parent 7630aa6874
commit 5c02ff6fd3

View File

@@ -2095,9 +2095,11 @@ fun MessageInputBar(
label = "record_ui_shift" label = "record_ui_shift"
) )
// ── Telegram-style recording layout ── // ── Telegram-exact recording layout ──
// Telegram uses separate overlay layers (RecordCircle 194dp, ControlsView 250dp) // RECORDING: [dot][timer] [◀ Slide to cancel] ... [Circle+Blob]
// We replicate with Box layers + graphicsLayer for overflow // LOCKED: [Delete] [Waveform 32dp] ... [Circle=Send] + Lock→Pause above
// PAUSED: [Delete] [Waveform 32dp] ... [Circle=Send] + Lock→Play above
// Timer and dot are HIDDEN in LOCKED/PAUSED (Telegram exact)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -2108,93 +2110,103 @@ fun MessageInputBar(
recordingInputRowY = coordinates.positionInWindow().y recordingInputRowY = coordinates.positionInWindow().y
} }
) { ) {
// ── Layer 1: Panel bar (timer + center) ── val isLockedOrPaused = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED
// Telegram: full-width bar, circle overlaps right edge
Row( // Crossfade between RECORDING panel and LOCKED panel
AnimatedContent(
targetState = isLockedOrPaused,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(40.dp) .height(44.dp)
.clip(RoundedCornerShape(20.dp)) .padding(end = 52.dp), // space for circle overlay
.background(recordingPanelColor) transitionSpec = {
.padding(start = 13.dp, end = 52.dp), // 52dp = half-circle overlap fadeIn(tween(200)) togetherWith fadeOut(tween(200))
verticalAlignment = Alignment.CenterVertically },
) { label = "record_panel_mode"
// Blink dot + timer ) { locked ->
Row( if (locked) {
modifier = Modifier // ── LOCKED/PAUSED panel (Telegram: recordedAudioPanel) ──
.graphicsLayer { // [Delete 44dp] [Waveform fills rest]
alpha = recordUiAlpha Row(
translationX = with(density) { recordUiShift.toPx() } modifier = Modifier
}, .fillMaxSize()
verticalAlignment = Alignment.CenterVertically .clip(RoundedCornerShape(22.dp))
) { .background(recordingPanelColor),
if (recordUiState == RecordUiState.PAUSED) { verticalAlignment = Alignment.CenterVertically
Box(modifier = Modifier.size(28.dp), contentAlignment = Alignment.Center) { ) {
Box(modifier = Modifier.size(10.dp).clip(CircleShape) // Delete button — Telegram: 44×44dp, Lottie trash icon
.background(if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D))) Box(
} modifier = Modifier
} else { .size(44.dp)
RecordBlinkDot(isDarkTheme = isDarkTheme) .clickable(
} interactionSource = remember { MutableInteractionSource() },
Spacer(modifier = Modifier.width(6.dp)) indication = null
Text( ) {
text = formatVoiceRecordTimer(voiceElapsedMs),
color = recordingTextColor,
fontSize = 15.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.width(12.dp))
// Center: SlideToCancel or Waveform+Controls
AnimatedContent(
targetState = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED,
modifier = Modifier
.weight(1f)
.graphicsLayer {
alpha = recordUiAlpha
translationX = with(density) { recordUiShift.toPx() }
},
transitionSpec = {
fadeIn(tween(200)) togetherWith fadeOut(tween(200))
},
label = "record_center_content"
) { isLockedOrPaused ->
if (isLockedOrPaused) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
VoiceWaveformBar(
waves = voiceWaves,
isDarkTheme = isDarkTheme,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
RecordLockedControls(
isPaused = recordUiState == RecordUiState.PAUSED,
isDarkTheme = isDarkTheme,
onDelete = {
inputJumpLog("tap DELETE (locked/paused) mode=$recordMode state=$recordUiState") inputJumpLog("tap DELETE (locked/paused) mode=$recordMode state=$recordUiState")
stopVoiceRecording(send = false) stopVoiceRecording(send = false)
}, },
onTogglePause = { contentAlignment = Alignment.Center
inputJumpLog("tap PAUSE/RESUME mode=$recordMode state=$recordUiState") ) {
if (recordUiState == RecordUiState.PAUSED) { Icon(
resumeVoiceRecording() imageVector = Icons.Default.Close,
} else { contentDescription = "Delete recording",
pauseVoiceRecording() tint = if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D),
} modifier = Modifier.size(20.dp)
}
) )
} }
} else {
// Waveform — Telegram: 32dp height, fills remaining width
VoiceWaveformBar(
waves = voiceWaves,
isDarkTheme = isDarkTheme,
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
)
}
} else {
// ── RECORDING panel ──
// [dot][timer] [◀ Slide to cancel]
Row(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(22.dp))
.background(recordingPanelColor)
.padding(start = 13.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Blink dot + timer
Row(
modifier = Modifier
.graphicsLayer {
alpha = recordUiAlpha
translationX = with(density) { recordUiShift.toPx() }
},
verticalAlignment = Alignment.CenterVertically
) {
RecordBlinkDot(isDarkTheme = isDarkTheme)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = formatVoiceRecordTimer(voiceElapsedMs),
color = recordingTextColor,
fontSize = 15.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.width(12.dp))
// Slide to cancel
SlideToCancel( SlideToCancel(
slideDx = slideDx, slideDx = slideDx,
cancelThresholdPx = cancelDragThresholdPx, cancelThresholdPx = cancelDragThresholdPx,
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.weight(1f)
.graphicsLayer {
alpha = recordUiAlpha
translationX = with(density) { recordUiShift.toPx() }
}
) )
} }
} }