From 17d6d1c855e28c669d7b0161cae301394fb0e279 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 31 Oct 2024 16:04:13 -0400 Subject: [PATCH 1/3] Use state to prevent double-clicking --- src/components/Pronunciations/AudioRecorder.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/Pronunciations/AudioRecorder.tsx b/src/components/Pronunciations/AudioRecorder.tsx index 981e949d27..8e81bd1622 100644 --- a/src/components/Pronunciations/AudioRecorder.tsx +++ b/src/components/Pronunciations/AudioRecorder.tsx @@ -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"; @@ -22,9 +22,20 @@ 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(); + useEffect(() => { + // Enable clicking only when the word id has changed + setClicked(false); + }, [props.id]); + async function startRecording(): Promise { + if (clicked) { + // Prevent clicking again before the word has updated with the first recording. + return; + } + const recordingId = recorder.getRecordingId(); if (recordingId && recordingId !== props.id) { // Prevent interfering with an active recording on a different entry. @@ -34,6 +45,8 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { // Prevent starting a recording before a previous one is finished. await stopRecording(); + setClicked(true); + if (!recorder.startRecording(props.id)) { let errorMessage = t("pronunciations.recordingError"); if (isBrowserFirefox()) { From beb09e5a3adf499448774aa26520daff3ae68e3e Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 1 Nov 2024 11:31:42 -0400 Subject: [PATCH 2/3] Add two more recording safeguards --- src/components/Pronunciations/AudioRecorder.tsx | 12 +++++++----- src/components/Pronunciations/RecorderIcon.tsx | 9 +++++---- .../Pronunciations/tests/RecorderIcon.test.tsx | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/Pronunciations/AudioRecorder.tsx b/src/components/Pronunciations/AudioRecorder.tsx index 8e81bd1622..d6364e6c43 100644 --- a/src/components/Pronunciations/AudioRecorder.tsx +++ b/src/components/Pronunciations/AudioRecorder.tsx @@ -26,20 +26,20 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { const { t } = useTranslation(); useEffect(() => { - // Enable clicking only when the word id has changed + // Re-enable clicking when the word id has changed. setClicked(false); }, [props.id]); - async function startRecording(): Promise { + async function startRecording(): Promise { if (clicked) { - // Prevent clicking again before the word has updated with the first recording. - return; + // 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; } // Prevent starting a recording before a previous one is finished. @@ -53,7 +53,9 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { errorMessage += ` ${t("pronunciations.recordingPermission")}`; } toast.error(errorMessage); + return false; } + return true; } async function stopRecording(): Promise { diff --git a/src/components/Pronunciations/RecorderIcon.tsx b/src/components/Pronunciations/RecorderIcon.tsx index 552c3e71d1..22899f1cd0 100644 --- a/src/components/Pronunciations/RecorderIcon.tsx +++ b/src/components/Pronunciations/RecorderIcon.tsx @@ -19,7 +19,7 @@ export const recordIconId = "recordingIcon"; interface RecorderIconProps { disabled?: boolean; id: string; - startRecording: () => void; + startRecording: () => Promise; stopRecording: () => void; } @@ -41,11 +41,12 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement { checkMicPermission().then(setHasMic); }, []); - function toggleIsRecordingToTrue(): void { + async function toggleIsRecordingToTrue(): Promise { 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, diff --git a/src/components/Pronunciations/tests/RecorderIcon.test.tsx b/src/components/Pronunciations/tests/RecorderIcon.test.tsx index 6fda104dac..03834390a4 100644 --- a/src/components/Pronunciations/tests/RecorderIcon.test.tsx +++ b/src/components/Pronunciations/tests/RecorderIcon.test.tsx @@ -31,7 +31,7 @@ function mockRecordingState(wordId: string): Partial { const mockWordId = "1234567890"; -const mockStartRecording = jest.fn(); +const mockStartRecording = jest.fn(() => Promise.resolve(true)); const mockStopRecording = jest.fn(); const renderRecorderIcon = async (wordId = ""): Promise => { From b87eca6b8718b2360f8d87ac0fe901126c8bbb37 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 1 Nov 2024 12:04:08 -0400 Subject: [PATCH 3/3] Handle stopRecording, clicked interaction --- src/components/Pronunciations/AudioRecorder.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Pronunciations/AudioRecorder.tsx b/src/components/Pronunciations/AudioRecorder.tsx index d6364e6c43..2e8af49740 100644 --- a/src/components/Pronunciations/AudioRecorder.tsx +++ b/src/components/Pronunciations/AudioRecorder.tsx @@ -26,7 +26,7 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { const { t } = useTranslation(); useEffect(() => { - // Re-enable clicking when the word id has changed. + // Re-enable clicking when the word id has changed setClicked(false); }, [props.id]); @@ -42,11 +42,11 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { return false; } + setClicked(true); + // Prevent starting a recording before a previous one is finished. await stopRecording(); - setClicked(true); - if (!recorder.startRecording(props.id)) { let errorMessage = t("pronunciations.recordingError"); if (isBrowserFirefox()) { @@ -58,7 +58,7 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { return true; } - async function stopRecording(): Promise { + async function stopRecording(): Promise { // Prevent triggering this function if no recording is active. if (recorder.getRecordingId() === undefined) { return; @@ -68,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) {