From 22ee59fd740275477bdb8145d2738a77ef6d5e04 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 10 Mar 2026 08:54:34 +0300 Subject: [PATCH] android: fix voice recording composer overlap --- android/CHANGELOG.md | 7 + .../messenger/ui/chat/ChatScreen.kt | 223 ++++++++++-------- 2 files changed, 136 insertions(+), 94 deletions(-) diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 3377f59..f4fef69 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -967,3 +967,10 @@ - Added dedicated read-only channel bottom bar (for non owner/admin): - compact Telegram-like controls with search + centered `Включить звук` action + notifications icon. - Kept existing full composer for roles allowed to post in channels (owner/admin). + +### Step 132 - Voice recording composer overlap fix +- Fixed composer overlap during voice recording: + - recording status/hint is now rendered in a dedicated top block inside composer, + - formatting toolbar is hidden while recording is active. +- Prevented controls collision for locked-recording actions: + - `Cancel/Send` now render on a separate row in locked state. diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 46fea63..ef60fe0 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -1114,76 +1114,86 @@ fun ChatScreen( .padding(horizontal = 8.dp, vertical = 6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(2.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - onClick = { - composerValue = applyInlineFormatting(composerValue, "**", "**") - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.FormatBold, contentDescription = "Bold") } - IconButton( - onClick = { - composerValue = applyInlineFormatting(composerValue, "*", "*") - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.FormatItalic, contentDescription = "Italic") } - IconButton( - onClick = { - composerValue = applyInlineFormatting(composerValue, "__", "__") - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.FormatUnderlined, contentDescription = "Underline") } - IconButton( - onClick = { - composerValue = applyInlineFormatting(composerValue, "~~", "~~") - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.StrikethroughS, contentDescription = "Strikethrough") } - IconButton( - onClick = { - composerValue = applyInlineFormatting(composerValue, "||", "||") - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.VisibilityOff, contentDescription = "Spoiler") } - IconButton( - onClick = { - composerValue = applyInlineFormatting(composerValue, "`", "`") - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.Code, contentDescription = "Monospace") } - IconButton( - onClick = { - composerValue = applyInlineFormatting(composerValue, "```\n", "\n```", "code") - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.Code, contentDescription = "Code block") } - IconButton( - onClick = { - composerValue = applyQuoteFormatting(composerValue) - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.FormatQuote, contentDescription = "Quote") } - IconButton( - onClick = { - composerValue = applyLinkFormatting(composerValue) - onInputChanged(composerValue.text) - }, - enabled = state.canSendMessages, - ) { Icon(Icons.Filled.Link, contentDescription = "Link") } + if (state.isRecordingVoice) { + VoiceRecordingStatusRow( + durationMs = state.voiceRecordingDurationMs, + hint = state.voiceRecordingHint, + isLocked = state.isVoiceLocked, + onCancel = onVoiceRecordCancel, + onSend = onVoiceRecordSend, + ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + onClick = { + composerValue = applyInlineFormatting(composerValue, "**", "**") + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.FormatBold, contentDescription = "Bold") } + IconButton( + onClick = { + composerValue = applyInlineFormatting(composerValue, "*", "*") + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.FormatItalic, contentDescription = "Italic") } + IconButton( + onClick = { + composerValue = applyInlineFormatting(composerValue, "__", "__") + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.FormatUnderlined, contentDescription = "Underline") } + IconButton( + onClick = { + composerValue = applyInlineFormatting(composerValue, "~~", "~~") + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.StrikethroughS, contentDescription = "Strikethrough") } + IconButton( + onClick = { + composerValue = applyInlineFormatting(composerValue, "||", "||") + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.VisibilityOff, contentDescription = "Spoiler") } + IconButton( + onClick = { + composerValue = applyInlineFormatting(composerValue, "`", "`") + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.Code, contentDescription = "Monospace") } + IconButton( + onClick = { + composerValue = applyInlineFormatting(composerValue, "```\n", "\n```", "code") + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.Code, contentDescription = "Code block") } + IconButton( + onClick = { + composerValue = applyQuoteFormatting(composerValue) + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.FormatQuote, contentDescription = "Quote") } + IconButton( + onClick = { + composerValue = applyLinkFormatting(composerValue) + onInputChanged(composerValue.text) + }, + enabled = state.canSendMessages, + ) { Icon(Icons.Filled.Link, contentDescription = "Link") } + } } Row( @@ -1267,30 +1277,6 @@ fun ChatScreen( } } } - if (state.isRecordingVoice) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp, vertical = 6.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "Voice ${formatDuration(state.voiceRecordingDurationMs.toInt())}", - style = MaterialTheme.typography.labelMedium, - ) - Text( - text = state.voiceRecordingHint ?: "", - style = MaterialTheme.typography.labelSmall, - ) - if (state.isVoiceLocked) { - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - Button(onClick = onVoiceRecordCancel) { Text("Cancel") } - Button(onClick = onVoiceRecordSend) { Text("Send") } - } - } - } - } } } @@ -2138,6 +2124,55 @@ private fun formatDateSeparatorLabel(date: LocalDate): String { } } +@Composable +private fun VoiceRecordingStatusRow( + durationMs: Long, + hint: String?, + isLocked: Boolean, + onCancel: () -> Unit, + onSend: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 2.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Voice ${formatDuration(durationMs.toInt())}", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + ) + if (!hint.isNullOrBlank()) { + Text( + text = hint, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, + ) + } + } + if (isLocked) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = onCancel) { Text("Cancel") } + Button(onClick = onSend) { Text("Send") } + } + } + } + } +} + private fun TextFieldValue.insertAtCursor(value: String): TextFieldValue { val start = selection.min val end = selection.max