Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ open class MessageListPage {
companion object {
val inputField get() = By.res("Stream_ComposerInputField")
val sendButton get() = By.res("Stream_ComposerSendButton")
val recordAudioButton get() = By.res("Stream_ComposerRecordAudioButton")
val recordAudioButton get() = By.res("Stream_ComposerAudioRecordingButton")
val commandsButton get() = By.res("Stream_ComposerCommandsButton")
val suggestionList get() = By.res("Stream_SuggestionList")
val suggestionListTitle get() = By.res("Stream_SuggestionListTitle")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,10 @@ class MessagesActivity : ComponentActivity() {
val message = composerViewModel.buildNewMessage(input, attachments)
composerViewModel.sendMessage(message)
},
recordingActions = AudioRecordingActions.defaultActions(composerViewModel),
recordingActions = AudioRecordingActions.defaultActions(
viewModel = composerViewModel,
sendOnComplete = ChatTheme.messageComposerTheme.audioRecording.sendOnComplete,
),
centerContent = { modifier -> ComposerTextInput(modifier, composerState) },
trailingContent = { ComposerTrailingIcon() },
)
Expand Down
60 changes: 30 additions & 30 deletions stream-chat-android-compose/api/stream-chat-android-compose.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
Expand All @@ -50,6 +51,7 @@ import io.getstream.chat.android.previewdata.PreviewMessageData
import io.getstream.chat.android.ui.common.state.messages.Edit
import io.getstream.chat.android.ui.common.state.messages.Reply
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState

/**
* Input field for the Messages/Conversation screen. Allows label customization, as well as handlers
Expand Down Expand Up @@ -129,7 +131,12 @@ public fun MessageInput(
) {
leadingContent()

centerContent(Modifier.weight(1f))
val isRecording = messageComposerState.recording !is RecordingState.Idle
if (!isRecording) {
centerContent(Modifier.weight(1f))
} else {
Spacer(Modifier.weight(1f))
}

trailingContent()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public fun SuggestionList(
headerContent: @Composable () -> Unit = {},
centerContent: @Composable () -> Unit,
) {
Popup(popupPositionProvider = AboveAnchorPopupPositionProvider()) {
Popup(popupPositionProvider = AboveAnchorPopupPositionProvider) {
Card(
modifier = modifier.semantics { testTagsAsResourceId = true },
elevation = CardDefaults.cardElevation(defaultElevation = ChatTheme.dimens.suggestionListElevation),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
Expand All @@ -40,14 +39,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import io.getstream.chat.android.compose.R
import io.getstream.chat.android.compose.ui.components.composer.MessageInput
import io.getstream.chat.android.compose.ui.messages.composer.actions.AudioRecordingActions
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.ui.theme.LocalMessageComposerFloatingStyleEnabled
import io.getstream.chat.android.compose.ui.theme.StreamTokens
import io.getstream.chat.android.compose.ui.util.AboveAnchorPopupPositionProvider
import io.getstream.chat.android.compose.ui.util.SnackbarPopup
import io.getstream.chat.android.compose.util.extensions.toSet
import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel
import io.getstream.chat.android.models.Attachment
Expand All @@ -58,7 +56,6 @@ import io.getstream.chat.android.models.Message
import io.getstream.chat.android.models.User
import io.getstream.chat.android.ui.common.state.messages.Edit
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState
import io.getstream.chat.android.ui.common.state.messages.composer.ValidationError
import io.getstream.chat.android.ui.common.utils.MediaStringUtil

Expand Down Expand Up @@ -88,8 +85,6 @@ import io.getstream.chat.android.ui.common.utils.MediaStringUtil
* @param commandPopupContent Customizable composable that represents the instant command suggestions popup.
* @param leadingContent The content shown at the start of the message composer.
* @param input Customizable composable that represents the input field for the composer, [MessageInput] by default.
* @param audioRecordingContent Customizable composable used for displaying audio recording information
* while audio recording is in progress.
* @param trailingContent Customizable composable that represents the trailing content of the composer, send button
* by default.
*/
Expand All @@ -108,7 +103,10 @@ public fun MessageComposer(
onMentionSelected: (User) -> Unit = { viewModel.selectMention(it) },
onCommandSelected: (Command) -> Unit = { viewModel.selectCommand(it) },
onAlsoSendToChannelSelected: (Boolean) -> Unit = { viewModel.setAlsoSendToChannel(it) },
recordingActions: AudioRecordingActions = AudioRecordingActions.defaultActions(viewModel),
recordingActions: AudioRecordingActions = AudioRecordingActions.defaultActions(
viewModel = viewModel,
sendOnComplete = ChatTheme.messageComposerTheme.audioRecording.sendOnComplete,
),
headerContent: @Composable ColumnScope.(MessageComposerState) -> Unit = {
with(ChatTheme.componentFactory) {
MessageComposerHeaderContent(
Expand Down Expand Up @@ -186,14 +184,6 @@ public fun MessageComposer(
)
}
},
audioRecordingContent: @Composable RowScope.(MessageComposerState) -> Unit = {
with(ChatTheme.componentFactory) {
MessageComposerAudioRecordingContent(
state = it,
recordingActions = recordingActions,
)
}
},
trailingContent: @Composable (MessageComposerState) -> Unit = {
ChatTheme.componentFactory.MessageComposerTrailingContent(
state = it,
Expand All @@ -220,7 +210,6 @@ public fun MessageComposer(
commandPopupContent = commandPopupContent,
leadingContent = leadingContent,
input = input,
audioRecordingContent = audioRecordingContent,
trailingContent = trailingContent,
messageComposerState = messageComposerState,
onCancelAction = onCancelAction,
Expand Down Expand Up @@ -255,8 +244,6 @@ public fun MessageComposer(
* @param commandPopupContent Customizable composable that represents the instant command suggestions popup.
* @param leadingContent The content shown at the start of the message composer.
* @param input Customizable composable that represents the input field for the composer, [MessageInput] by default.
* @param audioRecordingContent Customizable composable used for displaying audio recording information
* while audio recording is in progress.
* @param trailingContent Customizable composable that represents the trailing content of the composer, send button
* by default.
*/
Expand Down Expand Up @@ -347,14 +334,6 @@ public fun MessageComposer(
)
}
},
audioRecordingContent: @Composable RowScope.(MessageComposerState) -> Unit = {
with(ChatTheme.componentFactory) {
MessageComposerAudioRecordingContent(
state = it,
recordingActions = recordingActions,
)
}
},
trailingContent: @Composable (MessageComposerState) -> Unit = {
ChatTheme.componentFactory.MessageComposerTrailingContent(
state = it,
Expand All @@ -367,8 +346,6 @@ public fun MessageComposer(
val commandSuggestions = messageComposerState.commandSuggestions
val snackbarHostState = remember { SnackbarHostState() }

val isRecording = messageComposerState.recording !is RecordingState.Idle

MessageInputValidationError(
validationErrors = validationErrors,
snackbarHostState = snackbarHostState,
Expand Down Expand Up @@ -402,18 +379,14 @@ public fun MessageComposer(

input(messageComposerState)

if (isRecording) {
audioRecordingContent(messageComposerState)
}

trailingContent(messageComposerState)
}

footerContent(messageComposerState)
}

if (snackbarHostState.currentSnackbarData != null) {
SnackbarPopup(snackbarHostState = snackbarHostState)
SnackbarPopup(hostState = snackbarHostState)
}

if (mentionSuggestions.isNotEmpty()) {
Expand Down Expand Up @@ -508,20 +481,6 @@ private fun MessageInputValidationError(validationErrors: List<ValidationError>,
}
}

/**
* A snackbar wrapped inside of a popup allowing it be
* displayed above the Composable it's anchored to.
*
* @param snackbarHostState The state of the snackbar host. Contains
* the snackbar data necessary to display the snackbar.
*/
@Composable
private fun SnackbarPopup(snackbarHostState: SnackbarHostState) {
Popup(popupPositionProvider = AboveAnchorPopupPositionProvider()) {
SnackbarHost(hostState = snackbarHostState)
}
}

@Preview
@Composable
private fun MessageComposerDefaultStylePreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,56 @@ package io.getstream.chat.android.compose.ui.messages.composer.actions
import androidx.compose.runtime.Immutable
import androidx.compose.ui.geometry.Offset
import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel
import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState

/**
* Represents the actions that can be performed on an audio recording.
* Actions that can be performed during an audio recording session.
*
* @property onStartRecording Handler when the user starts recording an audio message.
* @property onHoldRecording Handler when the user holds the recording button.
* @property onLockRecording Handler when the user locks the recording.
* @property onCancelRecording Handler when the user cancels the recording.
* @property onDeleteRecording Handler when the user deletes the recording.
* @property onStopRecording Handler when the user stops the recording.
* @property onCompleteRecording Handler when the user completes the recording.
* @property onToggleRecordingPlayback Handler when the user toggles the recording playback.
* @property onRecordingSliderDragStart Handler when the user starts dragging the recording slider.
* @property onRecordingSliderDragStop Handler when the user stops dragging the recording slider.
* @property onSendRecording Handler when the user sends the recording.
* Each property maps a user gesture or button tap to a handler.
* Override individual actions via [copy] to customise behaviour while keeping the rest at their defaults.
*
* @property onStartRecording Begins a new recording.
* Transitions from [RecordingState.Idle] to [RecordingState.Hold].
* Ignored when the current state is not [RecordingState.Idle].
* @property onHoldRecording Updates the drag offset while the user holds the record button.
* The [Offset] represents the constrained drag delta from the initial press position
* (negative x = cancel direction, negative y = lock direction).
* Only meaningful in [RecordingState.Hold].
* @property onLockRecording Locks the recording so it continues hands-free.
* Transitions from [RecordingState.Hold] to [RecordingState.Locked].
* @property onCancelRecording Discards the recording via the swipe-to-cancel gesture.
* Invoked when the user drags past the cancel threshold during [RecordingState.Hold].
* Transitions to [RecordingState.Idle].
* @property onDeleteRecording Discards the recording via the delete button.
* Invoked when the user taps the trash icon in [RecordingState.Locked] or [RecordingState.Overview].
* Transitions to [RecordingState.Idle].
* @property onStopRecording Stops the active microphone recording.
* Transitions from [RecordingState.Locked] to [RecordingState.Overview],
* where the user can review the waveform before confirming.
* @property onConfirmRecording Finalises the recording.
* Depending on configuration, this either sends the recording immediately
* or attaches it to the composer for manual sending.
* Invoked on finger release (when not locked) or when tapping the confirm button in
* [RecordingState.Locked] / [RecordingState.Overview].
* @property onToggleRecordingPlayback Toggles play / pause of the recorded audio.
* Only meaningful in [RecordingState.Overview].
* @property onRecordingSliderDragStart Called when the user begins dragging the playback slider.
* Pauses playback. The [Float] is the playback progress at the drag start point (0..1).
* @property onRecordingSliderDragStop Called when the user releases the playback slider.
* Seeks playback to the given progress. The [Float] is the target progress (0..1).
*/
@Immutable
public data class AudioRecordingActions(
val onStartRecording: (Offset) -> Unit,
val onStartRecording: () -> Unit,
val onHoldRecording: (Offset) -> Unit,
val onLockRecording: () -> Unit,
val onCancelRecording: () -> Unit,
val onDeleteRecording: () -> Unit,
val onStopRecording: () -> Unit,
val onCompleteRecording: (Boolean) -> Unit,
val onConfirmRecording: () -> Unit,
val onToggleRecordingPlayback: () -> Unit,
val onRecordingSliderDragStart: (Float) -> Unit,
val onRecordingSliderDragStop: (Float) -> Unit,
val onSendRecording: () -> Unit,
) {
public companion object {

Expand All @@ -61,32 +82,35 @@ public data class AudioRecordingActions(
onCancelRecording = {},
onDeleteRecording = {},
onStopRecording = {},
onCompleteRecording = {},
onConfirmRecording = {},
onToggleRecordingPlayback = {},
onRecordingSliderDragStart = {},
onRecordingSliderDragStop = {},
onSendRecording = {},
)

/**
* Default implementation of [AudioRecordingActions].
* Default implementation backed by [viewModel].
*
* @param viewModel The [MessageComposerViewModel] that drives recording state.
* @param sendOnComplete When `true`, [onConfirmRecording] sends the message immediately.
* When `false`, it attaches the recording to the composer for manual sending.
*/
public fun defaultActions(
viewModel: MessageComposerViewModel,
sendOnComplete: Boolean,
): AudioRecordingActions = AudioRecordingActions(
onStartRecording = { viewModel.startRecording(it.toRestrictedCoordinates()) },
onStartRecording = viewModel::startRecording,
onHoldRecording = { viewModel.holdRecording(it.toRestrictedCoordinates()) },
onLockRecording = { viewModel.lockRecording() },
onCancelRecording = { viewModel.cancelRecording() },
onDeleteRecording = { viewModel.cancelRecording() },
onStopRecording = { viewModel.stopRecording() },
onCompleteRecording = { sendOnComplete ->
onLockRecording = viewModel::lockRecording,
onCancelRecording = viewModel::cancelRecording,
onDeleteRecording = viewModel::cancelRecording,
onStopRecording = viewModel::stopRecording,
onConfirmRecording = {
if (sendOnComplete) viewModel.sendRecording() else viewModel.completeRecording()
},
onToggleRecordingPlayback = { viewModel.toggleRecordingPlayback() },
onToggleRecordingPlayback = viewModel::toggleRecordingPlayback,
onRecordingSliderDragStart = { viewModel.pauseRecording() },
onRecordingSliderDragStop = { viewModel.seekRecordingTo(it) },
onSendRecording = { viewModel.sendRecording() },
onRecordingSliderDragStop = viewModel::seekRecordingTo,
)
}
}
Expand Down
Loading
Loading