signal-desktop/ts/state/ducks/audioRecorder.ts

261 lines
6 KiB
TypeScript
Raw Normal View History

2021-09-29 20:23:06 +00:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk';
2021-09-29 20:23:06 +00:00
import * as log from '../../logging/log';
import type { InMemoryAttachmentDraftType } from '../../types/Attachment';
2021-09-29 20:23:06 +00:00
import { SignalService as Proto } from '../../protobuf';
import type { StateType as RootStateType } from '../reducer';
2021-09-29 20:23:06 +00:00
import { fileToBytes } from '../../util/fileToBytes';
import { recorder } from '../../services/audioRecorder';
import { stringToMIMEType } from '../../types/MIME';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
2021-09-29 20:23:06 +00:00
import { useBoundActions } from '../../hooks/useBoundActions';
import { getComposerStateForConversation } from './composer';
2021-09-29 20:23:06 +00:00
export enum ErrorDialogAudioRecorderType {
Blur,
ErrorRecording,
2021-09-29 20:23:06 +00:00
Timeout,
}
// State
export enum RecordingState {
Recording = 'recording',
Initializing = 'initializing',
Idle = 'idle',
}
2021-09-29 20:23:06 +00:00
export type AudioPlayerStateType = {
readonly recordingState: RecordingState;
2021-09-29 20:23:06 +00:00
readonly errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
};
// Actions
const CANCEL_RECORDING = 'audioRecorder/CANCEL_RECORDING';
const COMPLETE_RECORDING = 'audioRecorder/COMPLETE_RECORDING';
const ERROR_RECORDING = 'audioRecorder/ERROR_RECORDING';
const NOW_RECORDING = 'audioRecorder/NOW_RECORDING';
2021-09-29 20:23:06 +00:00
const START_RECORDING = 'audioRecorder/START_RECORDING';
type CancelRecordingAction = {
type: typeof CANCEL_RECORDING;
payload: undefined;
};
type CompleteRecordingAction = {
type: typeof COMPLETE_RECORDING;
payload: undefined;
};
type ErrorRecordingAction = {
type: typeof ERROR_RECORDING;
payload: ErrorDialogAudioRecorderType;
};
type StartRecordingAction = {
type: typeof START_RECORDING;
payload: undefined;
};
type NowRecordingAction = {
type: typeof NOW_RECORDING;
payload: undefined;
};
2021-09-29 20:23:06 +00:00
type AudioPlayerActionType =
| CancelRecordingAction
| CompleteRecordingAction
| ErrorRecordingAction
| NowRecordingAction
2021-09-29 20:23:06 +00:00
| StartRecordingAction;
// Action Creators
export const actions = {
cancelRecording,
completeRecording,
errorRecording,
startRecording,
};
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
useBoundActions(actions);
2021-09-29 20:23:06 +00:00
function startRecording(
conversationId: string
): ThunkAction<
2021-09-29 20:23:06 +00:00
void,
RootStateType,
unknown,
StartRecordingAction | NowRecordingAction | ErrorRecordingAction
2021-09-29 20:23:06 +00:00
> {
return async (dispatch, getState) => {
const state = getState();
if (
getComposerStateForConversation(state.composer, conversationId)
.attachments.length
) {
2021-09-29 20:23:06 +00:00
return;
}
if (state.audioRecorder.recordingState !== RecordingState.Idle) {
return;
}
2021-09-29 20:23:06 +00:00
dispatch({
type: START_RECORDING,
payload: undefined,
});
try {
const started = await recorder.start();
if (started) {
dispatch({
type: NOW_RECORDING,
payload: undefined,
});
} else {
dispatch({
type: ERROR_RECORDING,
payload: ErrorDialogAudioRecorderType.ErrorRecording,
});
}
} catch (err) {
dispatch({
type: ERROR_RECORDING,
payload: ErrorDialogAudioRecorderType.ErrorRecording,
});
}
2021-09-29 20:23:06 +00:00
};
}
function completeRecordingAction(): CompleteRecordingAction {
return {
type: COMPLETE_RECORDING,
payload: undefined,
};
}
function completeRecording(
conversationId: string,
onSendAudioRecording?: (rec: InMemoryAttachmentDraftType) => unknown
2021-09-29 20:23:06 +00:00
): ThunkAction<
void,
RootStateType,
unknown,
CancelRecordingAction | CompleteRecordingAction
> {
return async (dispatch, getState) => {
const state = getState();
const isSelectedConversation =
state.conversations.selectedConversationId === conversationId;
if (!isSelectedConversation) {
log.warn(
'completeRecording: Recording started in one conversation and completed in another'
);
dispatch(cancelRecording());
return;
}
const blob = await recorder.stop();
try {
if (!blob) {
throw new Error('completeRecording: no blob returned');
}
const data = await fileToBytes(blob);
const voiceNoteAttachment: InMemoryAttachmentDraftType = {
pending: false,
2021-09-29 20:23:06 +00:00
contentType: stringToMIMEType(blob.type),
data,
size: data.byteLength,
flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE,
};
if (onSendAudioRecording) {
onSendAudioRecording(voiceNoteAttachment);
}
} finally {
dispatch(completeRecordingAction());
}
};
}
function cancelRecording(): ThunkAction<
void,
RootStateType,
unknown,
CancelRecordingAction
> {
return async dispatch => {
await recorder.stop();
recorder.clear();
2021-09-29 20:23:06 +00:00
dispatch({
type: CANCEL_RECORDING,
payload: undefined,
});
2021-09-29 20:23:06 +00:00
};
}
function errorRecording(
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType
): ErrorRecordingAction {
void recorder.stop();
2021-09-29 20:23:06 +00:00
return {
type: ERROR_RECORDING,
payload: errorDialogAudioRecorderType,
};
}
// Reducer
export function getEmptyState(): AudioPlayerStateType {
2021-09-29 20:23:06 +00:00
return {
recordingState: RecordingState.Idle,
2021-09-29 20:23:06 +00:00
};
}
export function reducer(
state: Readonly<AudioPlayerStateType> = getEmptyState(),
action: Readonly<AudioPlayerActionType>
): AudioPlayerStateType {
if (action.type === START_RECORDING) {
return {
...state,
errorDialogAudioRecorderType: undefined,
recordingState: RecordingState.Initializing,
};
}
if (action.type === NOW_RECORDING) {
return {
...state,
errorDialogAudioRecorderType: undefined,
recordingState: RecordingState.Recording,
2021-09-29 20:23:06 +00:00
};
}
if (action.type === CANCEL_RECORDING || action.type === COMPLETE_RECORDING) {
return {
...state,
errorDialogAudioRecorderType: undefined,
recordingState: RecordingState.Idle,
2021-09-29 20:23:06 +00:00
};
}
if (action.type === ERROR_RECORDING) {
return {
...state,
errorDialogAudioRecorderType: action.payload,
};
}
return state;
}