android: fix voice recording composer overlap
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user