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):
- 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.

View File

@@ -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