Record and send voice messages (#1596)
--------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
503efbf4c0
commit
b476654489
68 changed files with 2274 additions and 82 deletions
|
|
@ -64,7 +64,8 @@ import io.element.android.libraries.testtags.testTag
|
|||
import io.element.android.libraries.textcomposer.components.ComposerOptionsButton
|
||||
import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton
|
||||
import io.element.android.libraries.textcomposer.components.RecordButton
|
||||
import io.element.android.libraries.textcomposer.components.RecordingProgress
|
||||
import io.element.android.libraries.textcomposer.components.VoiceMessagePreview
|
||||
import io.element.android.libraries.textcomposer.components.VoiceMessageRecording
|
||||
import io.element.android.libraries.textcomposer.components.SendButton
|
||||
import io.element.android.libraries.textcomposer.components.TextFormatting
|
||||
import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape
|
||||
|
|
@ -95,6 +96,7 @@ fun TextComposer(
|
|||
onAddAttachment: () -> Unit = {},
|
||||
onDismissTextFormatting: () -> Unit = {},
|
||||
onVoiceRecordButtonEvent: (PressEvent) -> Unit = {},
|
||||
onSendVoiceMessage: () -> Unit = {},
|
||||
onError: (Throwable) -> Unit = {},
|
||||
) {
|
||||
val onSendClicked = {
|
||||
|
|
@ -137,24 +139,39 @@ fun TextComposer(
|
|||
composerMode = composerMode,
|
||||
)
|
||||
}
|
||||
val recordButton = @Composable {
|
||||
val recordVoiceButton = @Composable {
|
||||
RecordButton(
|
||||
onPressStart = { onVoiceRecordButtonEvent(PressEvent.PressStart) },
|
||||
onLongPressEnd = { onVoiceRecordButtonEvent(PressEvent.LongPressEnd) },
|
||||
onTap = { onVoiceRecordButtonEvent(PressEvent.Tapped) },
|
||||
)
|
||||
}
|
||||
val sendVoiceButton = @Composable {
|
||||
SendButton(
|
||||
canSendMessage = voiceMessageState is VoiceMessageState.Preview,
|
||||
onClick = { onSendVoiceMessage() },
|
||||
composerMode = composerMode,
|
||||
)
|
||||
}
|
||||
|
||||
val textFormattingOptions = @Composable { TextFormatting(state = state) }
|
||||
|
||||
val sendOrRecordButton = if (canSendMessage || !enableVoiceMessages) {
|
||||
sendButton
|
||||
} else {
|
||||
recordButton
|
||||
val sendOrRecordButton = when {
|
||||
enableVoiceMessages && !canSendMessage ->
|
||||
when (voiceMessageState) {
|
||||
is VoiceMessageState.Preview -> sendVoiceButton
|
||||
else -> recordVoiceButton
|
||||
}
|
||||
else ->
|
||||
sendButton
|
||||
}
|
||||
|
||||
val recordingProgress = @Composable {
|
||||
RecordingProgress()
|
||||
val voiceRecording = @Composable {
|
||||
if (voiceMessageState is VoiceMessageState.Recording) {
|
||||
VoiceMessageRecording(voiceMessageState.level)
|
||||
} else if (voiceMessageState is VoiceMessageState.Preview) {
|
||||
VoiceMessagePreview()
|
||||
}
|
||||
}
|
||||
|
||||
if (showTextFormatting) {
|
||||
|
|
@ -170,11 +187,12 @@ fun TextComposer(
|
|||
} else {
|
||||
StandardLayout(
|
||||
voiceMessageState = voiceMessageState,
|
||||
enableVoiceMessages = enableVoiceMessages,
|
||||
modifier = layoutModifier,
|
||||
composerOptionsButton = composerOptionsButton,
|
||||
textInput = textInput,
|
||||
endButton = sendOrRecordButton,
|
||||
recordingProgress = recordingProgress,
|
||||
voiceRecording = voiceRecording,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -190,9 +208,10 @@ fun TextComposer(
|
|||
@Composable
|
||||
private fun StandardLayout(
|
||||
voiceMessageState: VoiceMessageState,
|
||||
enableVoiceMessages: Boolean,
|
||||
textInput: @Composable () -> Unit,
|
||||
composerOptionsButton: @Composable () -> Unit,
|
||||
recordingProgress: @Composable () -> Unit,
|
||||
voiceRecording: @Composable () -> Unit,
|
||||
endButton: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -200,13 +219,13 @@ private fun StandardLayout(
|
|||
modifier = modifier,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
if (voiceMessageState is VoiceMessageState.Recording) {
|
||||
if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
recordingProgress()
|
||||
voiceRecording()
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -17,14 +17,10 @@
|
|||
package io.element.android.libraries.textcomposer.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -36,7 +32,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@Composable
|
||||
internal fun RecordingProgress(
|
||||
internal fun VoiceMessagePreview(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
|
|
@ -50,16 +46,9 @@ internal fun RecordingProgress(
|
|||
.heightIn(26.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape)
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
// TODO Replace with timer UI
|
||||
// TODO Replace with recording preview UI
|
||||
Text(
|
||||
text = "Recording...", // Not localized because it is a placeholder
|
||||
text = "Finished recording", // Not localized because it is a placeholder
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodySmMedium
|
||||
)
|
||||
|
|
@ -68,6 +57,6 @@ internal fun RecordingProgress(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RecordingProgressPreview() = ElementPreview {
|
||||
RecordingProgress()
|
||||
internal fun VoiceMessagePreviewPreview() = ElementPreview {
|
||||
VoiceMessagePreview()
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.textcomposer.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@Composable
|
||||
internal fun VoiceMessageRecording(
|
||||
level: Double,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
.padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp)
|
||||
.heightIn(26.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape)
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
// TODO Replace with timer UI
|
||||
Text(
|
||||
text = "Recording...", // Not localized because it is a placeholder
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodySmMedium
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(20.dp))
|
||||
|
||||
// TODO Replace with waveform UI
|
||||
DebugAudioLevel(
|
||||
modifier = Modifier.weight(1f), level = level
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugAudioLevel(
|
||||
level: Double,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(26.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxWidth(level.toFloat())
|
||||
.background(ElementTheme.colors.iconQuaternary, shape = MaterialTheme.shapes.small)
|
||||
.fillMaxHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun VoiceMessageRecordingPreview() = ElementPreview {
|
||||
VoiceMessageRecording(0.5)
|
||||
}
|
||||
|
|
@ -18,5 +18,9 @@ package io.element.android.libraries.textcomposer.model
|
|||
|
||||
sealed class VoiceMessageState {
|
||||
data object Idle: VoiceMessageState()
|
||||
data object Recording: VoiceMessageState()
|
||||
|
||||
data object Preview: VoiceMessageState()
|
||||
data class Recording(
|
||||
val level: Double,
|
||||
): VoiceMessageState()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue