android: fix voice recording composer overlap
Some checks failed
Android CI / android (push) Has started running
Android Release / release (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
Codex
2026-03-10 08:54:34 +03:00
parent f7ef10b011
commit 22ee59fd74
2 changed files with 136 additions and 94 deletions

View File

@@ -967,3 +967,10 @@
- Added dedicated read-only channel bottom bar (for non owner/admin): - Added dedicated read-only channel bottom bar (for non owner/admin):
- compact Telegram-like controls with search + centered `Включить звук` action + notifications icon. - compact Telegram-like controls with search + centered `Включить звук` action + notifications icon.
- Kept existing full composer for roles allowed to post in channels (owner/admin). - 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.

View File

@@ -1114,76 +1114,86 @@ fun ChatScreen(
.padding(horizontal = 8.dp, vertical = 6.dp), .padding(horizontal = 8.dp, vertical = 6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
) { ) {
Row( if (state.isRecordingVoice) {
modifier = Modifier VoiceRecordingStatusRow(
.fillMaxWidth() durationMs = state.voiceRecordingDurationMs,
.horizontalScroll(rememberScrollState()), hint = state.voiceRecordingHint,
horizontalArrangement = Arrangement.spacedBy(2.dp), isLocked = state.isVoiceLocked,
verticalAlignment = Alignment.CenterVertically, onCancel = onVoiceRecordCancel,
) { onSend = onVoiceRecordSend,
IconButton( )
onClick = { } else {
composerValue = applyInlineFormatting(composerValue, "**", "**") Row(
onInputChanged(composerValue.text) modifier = Modifier
}, .fillMaxWidth()
enabled = state.canSendMessages, .horizontalScroll(rememberScrollState()),
) { Icon(Icons.Filled.FormatBold, contentDescription = "Bold") } horizontalArrangement = Arrangement.spacedBy(2.dp),
IconButton( verticalAlignment = Alignment.CenterVertically,
onClick = { ) {
composerValue = applyInlineFormatting(composerValue, "*", "*") IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyInlineFormatting(composerValue, "**", "**")
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.FormatItalic, contentDescription = "Italic") } },
IconButton( enabled = state.canSendMessages,
onClick = { ) { Icon(Icons.Filled.FormatBold, contentDescription = "Bold") }
composerValue = applyInlineFormatting(composerValue, "__", "__") IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyInlineFormatting(composerValue, "*", "*")
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.FormatUnderlined, contentDescription = "Underline") } },
IconButton( enabled = state.canSendMessages,
onClick = { ) { Icon(Icons.Filled.FormatItalic, contentDescription = "Italic") }
composerValue = applyInlineFormatting(composerValue, "~~", "~~") IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyInlineFormatting(composerValue, "__", "__")
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.StrikethroughS, contentDescription = "Strikethrough") } },
IconButton( enabled = state.canSendMessages,
onClick = { ) { Icon(Icons.Filled.FormatUnderlined, contentDescription = "Underline") }
composerValue = applyInlineFormatting(composerValue, "||", "||") IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyInlineFormatting(composerValue, "~~", "~~")
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.VisibilityOff, contentDescription = "Spoiler") } },
IconButton( enabled = state.canSendMessages,
onClick = { ) { Icon(Icons.Filled.StrikethroughS, contentDescription = "Strikethrough") }
composerValue = applyInlineFormatting(composerValue, "`", "`") IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyInlineFormatting(composerValue, "||", "||")
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.Code, contentDescription = "Monospace") } },
IconButton( enabled = state.canSendMessages,
onClick = { ) { Icon(Icons.Filled.VisibilityOff, contentDescription = "Spoiler") }
composerValue = applyInlineFormatting(composerValue, "```\n", "\n```", "code") IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyInlineFormatting(composerValue, "`", "`")
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.Code, contentDescription = "Code block") } },
IconButton( enabled = state.canSendMessages,
onClick = { ) { Icon(Icons.Filled.Code, contentDescription = "Monospace") }
composerValue = applyQuoteFormatting(composerValue) IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyInlineFormatting(composerValue, "```\n", "\n```", "code")
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.FormatQuote, contentDescription = "Quote") } },
IconButton( enabled = state.canSendMessages,
onClick = { ) { Icon(Icons.Filled.Code, contentDescription = "Code block") }
composerValue = applyLinkFormatting(composerValue) IconButton(
onInputChanged(composerValue.text) onClick = {
}, composerValue = applyQuoteFormatting(composerValue)
enabled = state.canSendMessages, onInputChanged(composerValue.text)
) { Icon(Icons.Filled.Link, contentDescription = "Link") } },
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( 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 { private fun TextFieldValue.insertAtCursor(value: String): TextFieldValue {
val start = selection.min val start = selection.min
val end = selection.max val end = selection.max