Audio recorder
The audio recorder is the part of MessageComposer that records voice messages. The record button, AudioRecordingButton, replaces the send button while the composer is empty, and the recorder parts take the input's place while a recording runs. All of them appear only when audioRecordingEnabled is set on the Channel or Thread. Voice recordings and audio playback explains how a recording is made and sent.
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
<Channel audioRecordingEnabled channel={channel}>
<MessageList />
<MessageComposer />
</Channel>
);Best practices
- Turn recording on for each
ChannelandThreadthat should have it. A thread screen needsaudioRecordingEnabledon itsThreadtoo. - Install an audio package and allow uploads in the channel. The record button checks neither; see Turning it on or off.
- Set the gesture distances and the press duration on
ChannelorThread. The record button reads them fromMessageInputContext, and the same props onMessageComposerdon't reach it. - Replace the parts through their override keys. A part of your own reads the recorder from
audioRecorderManagerinuseMessageInputContext(), and calls the context's recording functions. - Keep the default record button unless you replace the whole gesture. Its press, slide-to-cancel and slide-to-lock gestures drive the other parts.
UI customization
| Override key | Component | Replaces | Props |
|---|---|---|---|
StartAudioRecordingButton |
AudioRecordingButton |
The record button | Below |
AudioRecorder |
AudioRecorder |
The input, while a recording runs: the time and "Slide to Cancel", or the buttons of a locked recording | Below |
AudioRecordingLockIndicator |
AudioRecordingLockIndicator |
The lock mark above the record button, while a recording runs | Below |
AudioRecordingInProgress |
AudioRecordingInProgress |
The time and the live waveform above the input, while a locked recording runs | Below |
AudioRecordingWaveform |
AudioRecordingWaveform |
The live waveform inside AudioRecordingInProgress |
Below |
AudioRecordingPreview |
AudioRecordingPreview |
The player above the input, after a locked recording is stopped | none |
AudioAttachmentUploadPreview |
AudioAttachmentUploadPreview |
A finished recording waiting in the composer | Composer UI components |
What the composer shows during a recording:
- Holding the button.
AudioRecorderreplaces the input with a red microphone, the elapsed time and "Slide to Cancel", which fades as the user slides to the left.AudioRecordingLockIndicatorshows an open lock and an arrow above the button. The previews above the input and the attach button are hidden. - Locked.
AudioRecordingInProgressshows the time and the waveform above the input, andAudioRecordershows three buttons: delete, stop and send. The lock indicator shows a closed lock, then disappears after two seconds. - Stopped.
AudioRecordingPreviewplays the recording, with a play button, the time and a waveform the user can drag to seek.AudioRecordershows delete and send.
The delete button publishes a "Voice message deleted" notification. The send button calls uploadVoiceRecording(audioRecordingSendOnComplete), which adds it to the composer or sends it at once.
AudioRecordingButton props
AudioRecordingButton reads its props from MessageInputContext and the recorder's state. Pass one to override it.
A press shorter than asyncMessagesMinimumPressDuration publishes "Hold to record. Release to save." A longer press pauses the audio that is playing and starts recording. When the microphone permission is denied, it publishes a notification with a button that opens the settings. While the user holds it, the button tracks the finger:
- Releasing it after sliding to the left by more than half of
asyncMessagesSlideToCancelDistancedeletes the recording. - Releasing it after sliding up by more than half of
asyncMessagesLockDistancelocks the recording. - Releasing it otherwise completes the recording with
uploadVoiceRecording(audioRecordingSendOnComplete), or deletes it when it's shorter than 300 milliseconds.
| Prop | Type | Default | Description |
|---|---|---|---|
handlePress |
() => void |
none | Called on a short press, before the "Hold to record" notification, which still shows while nothing is recorded. |
handleLongPress |
() => void |
none | Called on a long press, in place of starting the recording. |
asyncMessagesMinimumPressDuration |
number |
from MessageInputContext, 500 |
How long, in milliseconds, the user holds the button to start recording. |
asyncMessagesSlideToCancelDistance |
number |
from MessageInputContext, 75 |
How far, in points, the finger slides to the left to cancel. |
asyncMessagesLockDistance |
number |
from MessageInputContext, 50 |
How far, in points, the finger slides up to lock. |
audioRecordingSendOnComplete |
boolean |
from MessageInputContext, false |
Passed to uploadVoiceRecording when the user releases the button. |
audioRecorderManager |
AudioRecorderManager |
from MessageInputContext |
The recorder whose micLocked the lock gesture sets. |
startVoiceRecording |
() => Promise<boolean | undefined> |
from MessageInputContext |
Starts recording on a long press. |
deleteVoiceRecording |
() => Promise<void> |
from MessageInputContext |
Deletes the recording on cancel and on an early release. |
uploadVoiceRecording |
(sendOnComplete: boolean) => Promise<void> |
from MessageInputContext |
Completes the recording on release. |
status |
RecordingStatusStates |
the recorder's status |
"idle", "recording" or "stopped". Locking works and release completes only while it's "recording". |
recording |
AudioRecordingReturnType |
the recorder's recording |
The recording in progress. While it's set, a short press shows no notification and a long press starts nothing. |
cancellableDuration |
boolean |
whether duration is under 300 ms |
While it's true, releasing the button deletes the recording. |
buttonSize |
number |
none | Not read. |
AudioRecorder props
AudioRecorder reads its props from MessageInputContext and the recorder's state. MessageComposer passes it slideToCancelStyle.
| Prop | Type | Default | Description |
|---|---|---|---|
slideToCancelStyle |
StyleProp<AnimatedStyle<ViewStyle>> |
none | The style of "Slide to Cancel". MessageComposer passes an animated opacity that fades it as the user slides. |
micLocked |
boolean |
the recorder's micLocked |
Shows the buttons of a locked recording instead of the time and "Slide to Cancel". |
status |
RecordingStatusStates |
the recorder's status |
While it's "stopped", the buttons of a locked recording leave out the stop button. |
duration |
number |
the recorder's duration |
The elapsed time, in milliseconds, shown as mm:ss. |
audioRecordingSendOnComplete |
boolean |
from MessageInputContext |
Passed to uploadVoiceRecording by the send button. |
deleteVoiceRecording |
() => Promise<void> |
from MessageInputContext |
Called by the delete button. |
stopVoiceRecording |
() => Promise<void> |
from MessageInputContext |
Called by the stop button. |
uploadVoiceRecording |
(sendOnComplete: boolean) => Promise<void> |
from MessageInputContext |
Called by the send button. |
audioRecorderManager |
AudioRecorderManager |
from MessageInputContext |
Not read. |
AudioRecordingLockIndicator props
MessageComposer renders AudioRecordingLockIndicator while a recording runs, with these props.
| Prop | Type | Default | Description |
|---|---|---|---|
micLocked |
boolean |
required | Shows a closed lock instead of an open lock and an arrow. The indicator disappears two seconds after it turns true. |
messageInputHeight |
number |
none | The composer's height. The indicator sits that far from the bottom. |
style |
StyleProp<AnimatedStyle<ViewStyle>> |
none | MessageComposer passes an animated translation that moves the indicator up with the finger. |
AudioRecordingInProgress props
AudioRecordingInProgress reads its props from MessageInputContext and the recorder's state. It renders AudioRecordingWaveform with maxDataPointsDrawn and waveformData.
| Prop | Type | Default | Description |
|---|---|---|---|
maxDataPointsDrawn |
number |
60 |
How many of the latest waveform samples are drawn. |
duration |
number |
the recorder's duration |
The elapsed time, in milliseconds, shown as mm:ss. |
waveformData |
number[] |
the recorder's waveformData |
The audio levels sampled so far, each from 0 to 1. |
audioRecorderManager |
AudioRecorderManager |
from MessageInputContext |
Not read. |
AudioRecordingWaveform props
| Prop | Type | Default | Description |
|---|---|---|---|
maxDataPointsDrawn |
number |
required | How many of the latest samples are drawn, one bar each. |
waveformData |
number[] |
required | The audio levels, each from 0 to 1. A bar's height is its level times 20. |
AudioRecordingPreview takes no props. It reads the recording, its duration and its waveform from the recorder, and plays it through the audio player pool of the Channel or Thread.
The recorder
audioRecorderManager in useMessageInputContext() is the recorder of the Channel or Thread. Its state store holds:
| Field | Type | Description |
|---|---|---|
status |
"idle" | "recording" | "stopped" |
"recording" while it records, "stopped" after the stop button, "idle" otherwise. |
duration |
number |
The elapsed time, in milliseconds. |
waveformData |
number[] |
The audio levels sampled so far, each from 0 to 1. |
micLocked |
boolean |
Whether the recording is locked. |
isStarting |
boolean |
Whether a recording is being set up. |
recording |
AudioRecordingReturnType |
The recording of the audio package, or its URI. |
The functions that act on it, startVoiceRecording(), stopVoiceRecording(), deleteVoiceRecording() and uploadVoiceRecording(sendOnComplete), are on MessageInputContext.
MessageComposer also provides MicPositionContext, which useMicPositionContext() reads. It holds micPositionX and micPositionY, Reanimated shared values with how far the finger has moved from the record button, in points. The record button writes them, and the composer animates "Slide to Cancel" and the lock indicator from them. A replacement for one of those parts can read them too.
Configuration on Channel and Thread
| Prop | Type | Default | Description |
|---|---|---|---|
audioRecordingEnabled |
boolean |
false |
Shows the record button while the composer has no text and no attachments. |
audioRecordingSendOnComplete |
boolean |
false |
Sends a finished recording at once, as a message of its own, instead of keeping it in the composer. |
asyncMessagesMinimumPressDuration |
number |
500 |
How long, in milliseconds, the user holds the record button to start recording. |
asyncMessagesSlideToCancelDistance |
number |
75 |
How far, in points, the user slides to the left to cancel. |
asyncMessagesLockDistance |
number |
50 |
How far, in points, the user slides up to lock. |
allowConcurrentAudioPlayback |
boolean |
false |
Lets several recordings play at once, including the preview of a stopped recording. |
audioRecordingSendOnComplete sends the recording before its upload has finished, so it needs messageComposer.attachments.pendingUploadsEnabled, which is off by default and which Chat turns on with enableOfflineSupport. The recording format comes from the audio package; to change it, pass overrideAudioRecordingConfiguration to registerNativeHandlers(), as Entry points and native handlers shows.
Examples
Show fewer waveform bars
The default AudioRecordingInProgress takes maxDataPointsDrawn. Register a component that renders it with the prop:
import type { AudioRecordingInProgressProps } from "stream-chat-react-native";
import { AudioRecordingInProgress } from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";
const ShortWaveformInProgress = (props: AudioRecordingInProgressProps) => (
<AudioRecordingInProgress {...props} maxDataPointsDrawn={30} />
);
// Pass to `WithComponents`, declared once at module scope.
export const recorderOverrides: ComponentOverrides = {
AudioRecordingInProgress: ShortWaveformInProgress,
};Show that a recording runs
Parts under the Channel or Thread subscribe to the recorder's store. This badge shows while the user records:
import { Text } from "react-native";
import {
useMessageInputContext,
useStateStore,
} from "stream-chat-react-native";
const recordingSelector = ({
duration,
status,
}: {
duration: number;
status: string;
}) => ({
seconds: Math.floor(duration / 1000),
isRecording: status === "recording",
});
export const RecordingBadge = () => {
const { audioRecorderManager } = useMessageInputContext();
const { isRecording, seconds } = useStateStore(
audioRecorderManager.state,
recordingSelector,
);
if (!isRecording) return null;
return <Text>Recording {seconds}s</Text>;
};The recorder replaces AudioRecorder with a recorder of its own.
Hooks
| Hook | Returns |
|---|---|
useMessageInputContext() |
audioRecorderManager and the recording functions; throws outside Channel and Thread |
useMicPositionContext() |
micPositionX and micPositionY, inside MessageComposer |
useActiveAudioPlayer() |
the audio player that is playing, or null |
useAudioRecorder() |
the recording functions for a recorder; Channel and Thread call it already |
See useMessageInputContext and useAudioRecorder.