Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

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.

src/ChannelScreen.tsx
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 Channel and Thread that should have it. A thread screen needs audioRecordingEnabled on its Thread too.
  • 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 Channel or Thread. The record button reads them from MessageInputContext, and the same props on MessageComposer don't reach it.
  • Replace the parts through their override keys. A part of your own reads the recorder from audioRecorderManager in useMessageInputContext(), 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:

  1. Holding the button. AudioRecorder replaces the input with a red microphone, the elapsed time and "Slide to Cancel", which fades as the user slides to the left. AudioRecordingLockIndicator shows an open lock and an arrow above the button. The previews above the input and the attach button are hidden.
  2. Locked. AudioRecordingInProgress shows the time and the waveform above the input, and AudioRecorder shows three buttons: delete, stop and send. The lock indicator shows a closed lock, then disappears after two seconds.
  3. Stopped. AudioRecordingPreview plays the recording, with a play button, the time and a waveform the user can drag to seek. AudioRecorder shows 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 asyncMessagesSlideToCancelDistance deletes the recording.
  • Releasing it after sliding up by more than half of asyncMessagesLockDistance locks 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:

src/recorderOverrides.tsx
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:

src/RecordingBadge.tsx
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.