Skip to content

Commit

Permalink
Prevent rapid-click audio recorder bugs (#3414)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Nov 5, 2024
1 parent e349e02 commit a4aa54c
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 10 deletions.
26 changes: 21 additions & 5 deletions src/components/Pronunciations/AudioRecorder.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactElement, useContext } from "react";
import { ReactElement, useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-toastify";

Expand All @@ -22,15 +22,28 @@ export default function AudioRecorder(props: RecorderProps): ReactElement {
(state: StoreState) => state.currentProjectState.speaker?.id
);
const recorder = useContext(RecorderContext);
const [clicked, setClicked] = useState(false);
const { t } = useTranslation();

async function startRecording(): Promise<void> {
useEffect(() => {
// Re-enable clicking when the word id has changed
setClicked(false);
}, [props.id]);

async function startRecording(): Promise<boolean> {
if (clicked) {
// Prevent recording again before this word has updated.
return false;
}

const recordingId = recorder.getRecordingId();
if (recordingId && recordingId !== props.id) {
// Prevent interfering with an active recording on a different entry.
return;
return false;
}

setClicked(true);

// Prevent starting a recording before a previous one is finished.
await stopRecording();

Expand All @@ -40,10 +53,12 @@ export default function AudioRecorder(props: RecorderProps): ReactElement {
errorMessage += ` ${t("pronunciations.recordingPermission")}`;
}
toast.error(errorMessage);
return false;
}
return true;
}

async function stopRecording(): Promise<string | undefined> {
async function stopRecording(): Promise<void> {
// Prevent triggering this function if no recording is active.
if (recorder.getRecordingId() === undefined) {
return;
Expand All @@ -53,8 +68,9 @@ export default function AudioRecorder(props: RecorderProps): ReactElement {
props.onClick();
}
const file = await recorder.stopRecording();
if (!file) {
if (!file || !file.size) {
toast.error(t("pronunciations.recordingError"));
setClicked(false);
return;
}
if (!props.noSpeaker) {
Expand Down
9 changes: 5 additions & 4 deletions src/components/Pronunciations/RecorderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const recordIconId = "recordingIcon";
interface RecorderIconProps {
disabled?: boolean;
id: string;
startRecording: () => void;
startRecording: () => Promise<boolean>;
stopRecording: () => void;
}

Expand All @@ -41,11 +41,12 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement {
checkMicPermission().then(setHasMic);
}, []);

function toggleIsRecordingToTrue(): void {
async function toggleIsRecordingToTrue(): Promise<void> {
if (!isRecording) {
// Only start a recording if there's not another on in progress.
dispatch(recording(props.id));
props.startRecording();
if (await props.startRecording()) {
dispatch(recording(props.id));
}
} else {
// This triggers if user clicks-and-holds on one entry's record icon,
// drags the mouse outside that icon before releasing,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Pronunciations/tests/RecorderIcon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function mockRecordingState(wordId: string): Partial<StoreState> {

const mockWordId = "1234567890";

const mockStartRecording = jest.fn();
const mockStartRecording = jest.fn(() => Promise.resolve(true));
const mockStopRecording = jest.fn();

const renderRecorderIcon = async (wordId = ""): Promise<void> => {
Expand Down

0 comments on commit a4aa54c

Please sign in to comment.