From ec875b31c748cf3a92ff743ee01156cb3ba6c9e3 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 12 Jul 2024 10:18:32 -0400 Subject: [PATCH 1/5] Change recording errors depending on mic permission --- public/locales/en/translation.json | 3 +- .../Pronunciations/AudioRecorder.tsx | 2 +- src/components/Pronunciations/Recorder.ts | 10 ++- .../Pronunciations/RecorderIcon.tsx | 65 +++++++++++-------- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 69b9b69a0f..9a138d9b88 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -469,6 +469,7 @@ "upload": "Upload" }, "pronunciations": { + "enableMicTooltip": "Enable microphone access to record.", "recordTooltip": "Press and hold to record.", "playTooltip": "Click to play.", "deleteTooltip": "Shift click to delete.", @@ -477,7 +478,7 @@ "speakerAdd": "Right click to add a speaker.", "speakerChange": "Right click to change speaker.", "speakerSelect": "Select speaker of this audio recording", - "noMicAccess": "Recording error: Could not access a microphone.", + "recordingError": "Recording error", "deleteRecording": "Delete Recording" }, "statistics": { diff --git a/src/components/Pronunciations/AudioRecorder.tsx b/src/components/Pronunciations/AudioRecorder.tsx index fcb8ad58ce..2d09cc4f5e 100644 --- a/src/components/Pronunciations/AudioRecorder.tsx +++ b/src/components/Pronunciations/AudioRecorder.tsx @@ -47,7 +47,7 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { } const file = await recorder.stopRecording(); if (!file) { - toast.error(t("pronunciations.noMicAccess")); + toast.error(t("pronunciations.recordingError")); return; } if (!props.noSpeaker) { diff --git a/src/components/Pronunciations/Recorder.ts b/src/components/Pronunciations/Recorder.ts index 38fe5b50d6..4271b0bf42 100644 --- a/src/components/Pronunciations/Recorder.ts +++ b/src/components/Pronunciations/Recorder.ts @@ -63,6 +63,14 @@ export default class Recorder { private onError(err: Error): void { console.error(err); - this.toast("Error getting audio stream!"); + navigator.permissions + .query({ name: "microphone" as PermissionName }) + .then((result) => { + this.toast( + result.state === "granted" + ? "Error getting audio stream!" + : "No microphone access." + ); + }); } } diff --git a/src/components/Pronunciations/RecorderIcon.tsx b/src/components/Pronunciations/RecorderIcon.tsx index 7ecd2422d3..c359d74df6 100644 --- a/src/components/Pronunciations/RecorderIcon.tsx +++ b/src/components/Pronunciations/RecorderIcon.tsx @@ -1,6 +1,6 @@ import { FiberManualRecord } from "@mui/icons-material"; import { IconButton, Tooltip } from "@mui/material"; -import { ReactElement } from "react"; +import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { @@ -33,8 +33,15 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement { const isRecordingThis = isRecording && recordingId === props.id; const dispatch = useAppDispatch(); + const [hasMic, setHasMic] = useState(false); const { t } = useTranslation(); + useEffect(() => { + navigator.permissions + .query({ name: "microphone" as PermissionName }) + .then((result) => setHasMic(result.state === "granted")); + }, []); + function toggleIsRecordingToTrue(): void { if (!isRecording) { // Only start a recording if there's not another on in progress. @@ -75,36 +82,42 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement { document.removeEventListener("contextmenu", disableContextMenu, false); } + const tooltipId = hasMic + ? "pronunciations.recordTooltip" + : "pronunciations.enableMicTooltip"; + return ( - - - props.disabled - ? t.palette.grey[400] - : isRecordingThis - ? themeColors.recordActive - : themeColors.recordIdle, - }} - /> - + + + + props.disabled || !hasMic + ? t.palette.grey[400] + : isRecordingThis + ? themeColors.recordActive + : themeColors.recordIdle, + }} + /> + + ); } From a364a4d0be2625756850820d76ed41a70517098b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 12 Jul 2024 10:42:53 -0400 Subject: [PATCH 2/5] Fix tests --- .../Pronunciations/tests/AudioRecorder.test.tsx | 8 ++++---- .../Pronunciations/tests/PronunciationsFrontend.test.tsx | 4 ++-- src/setupTests.js | 7 +++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/Pronunciations/tests/AudioRecorder.test.tsx b/src/components/Pronunciations/tests/AudioRecorder.test.tsx index 3f1e1b2cfb..b9653d69fd 100644 --- a/src/components/Pronunciations/tests/AudioRecorder.test.tsx +++ b/src/components/Pronunciations/tests/AudioRecorder.test.tsx @@ -25,8 +25,8 @@ function mockRecordingState(wordId: string): Partial { } describe("AudioRecorder", () => { - test("default icon style is idle", () => { - act(() => { + test("default icon style is idle", async () => { + await act(async () => { testRenderer = create( @@ -41,10 +41,10 @@ describe("AudioRecorder", () => { expect(icon.props.sx.color({})).toEqual(themeColors.recordIdle); }); - test("icon style depends on pronunciations state", () => { + test("icon style depends on pronunciations state", async () => { const wordId = "1"; const mockStore2 = configureMockStore()(mockRecordingState(wordId)); - act(() => { + await act(async () => { testRenderer = create( diff --git a/src/components/Pronunciations/tests/PronunciationsFrontend.test.tsx b/src/components/Pronunciations/tests/PronunciationsFrontend.test.tsx index 11d65a6382..e559277b85 100644 --- a/src/components/Pronunciations/tests/PronunciationsFrontend.test.tsx +++ b/src/components/Pronunciations/tests/PronunciationsFrontend.test.tsx @@ -15,9 +15,9 @@ let testRenderer: renderer.ReactTestRenderer; const mockStore = configureMockStore()(defaultState); describe("PronunciationsFrontend", () => { - it("renders with record button and play buttons", () => { + it("renders with record button and play buttons", async () => { const audio = ["a.wav", "b.wav"].map((f) => newPronunciation(f)); - renderer.act(() => { + await renderer.act(async () => { testRenderer = renderer.create( diff --git a/src/setupTests.js b/src/setupTests.js index 4c546243b2..48a80ee9ea 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -8,6 +8,13 @@ global.console.warn = (message) => { throw message; }; +// Mock all permissions as granted +Object.defineProperty(navigator, "permissions", { + get() { + return { query: () => Promise.resolve({ state: "granted" }) }; + }, +}); + // Mock the audio components jest .spyOn(window.HTMLMediaElement.prototype, "pause") From 33755c22d8f0a28e2aa4be0a4ca4840659419051 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 17 Jul 2024 14:47:09 -0400 Subject: [PATCH 3/5] Localize Recorder toast --- public/locales/en/translation.json | 4 +++- src/components/Pronunciations/Recorder.ts | 8 ++++---- src/components/Pronunciations/RecorderContext.ts | 5 ++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 9a138d9b88..f13a44b983 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -479,7 +479,9 @@ "speakerChange": "Right click to change speaker.", "speakerSelect": "Select speaker of this audio recording", "recordingError": "Recording error", - "deleteRecording": "Delete Recording" + "deleteRecording": "Delete Recording", + "audioStreamError": "Error getting audio stream!", + "noMicAccess": "No microphone access. Audio recording disabled." }, "statistics": { "title": "Data Statistics: {{ val }}", diff --git a/src/components/Pronunciations/Recorder.ts b/src/components/Pronunciations/Recorder.ts index 4271b0bf42..9985695c1e 100644 --- a/src/components/Pronunciations/Recorder.ts +++ b/src/components/Pronunciations/Recorder.ts @@ -3,13 +3,13 @@ import RecordRTC from "recordrtc"; import { getFileNameForWord } from "components/Pronunciations/utilities"; export default class Recorder { - private toast: (text: string) => void; + private toast: (textId: string) => void; private recordRTC?: RecordRTC; private id?: string; static blobType: RecordRTC.Options["type"] = "audio"; - constructor(toast?: (text: string) => void) { + constructor(toast?: (textId: string) => void) { this.toast = toast ?? ((text: string) => alert(text)); navigator.mediaDevices .getUserMedia({ audio: true }) @@ -68,8 +68,8 @@ export default class Recorder { .then((result) => { this.toast( result.state === "granted" - ? "Error getting audio stream!" - : "No microphone access." + ? "pronunciations.audioStreamError" + : "pronunciations.noMicAccess" ); }); } diff --git a/src/components/Pronunciations/RecorderContext.ts b/src/components/Pronunciations/RecorderContext.ts index 251a40094e..5a5d38c7b1 100644 --- a/src/components/Pronunciations/RecorderContext.ts +++ b/src/components/Pronunciations/RecorderContext.ts @@ -2,7 +2,10 @@ import { enqueueSnackbar } from "notistack"; import { createContext } from "react"; import Recorder from "components/Pronunciations/Recorder"; +import i18n from "i18n"; -const RecorderContext = createContext(new Recorder(enqueueSnackbar)); +const RecorderContext = createContext( + new Recorder((textId: string) => enqueueSnackbar(i18n.t(textId))) +); export default RecorderContext; From edb843be03e659f167f993851db5de7a28ce4cad Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 17 Jul 2024 15:02:12 -0400 Subject: [PATCH 4/5] Fix tests --- src/setupTests.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/setupTests.js b/src/setupTests.js index 48a80ee9ea..5b85f91d69 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -19,4 +19,5 @@ Object.defineProperty(navigator, "permissions", { jest .spyOn(window.HTMLMediaElement.prototype, "pause") .mockImplementation(() => {}); -jest.mock("components/Pronunciations/Recorder"); +//jest.mock("components/Pronunciations/Recorder"); +jest.mock("components/Pronunciations/RecorderContext", () => ({})); From c853d3222aff39561a819b9e39a888f097d03a87 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 17 Jul 2024 15:02:50 -0400 Subject: [PATCH 5/5] Remove old code --- src/setupTests.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/setupTests.js b/src/setupTests.js index 5b85f91d69..8c9e4d3de7 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -19,5 +19,4 @@ Object.defineProperty(navigator, "permissions", { jest .spyOn(window.HTMLMediaElement.prototype, "pause") .mockImplementation(() => {}); -//jest.mock("components/Pronunciations/Recorder"); jest.mock("components/Pronunciations/RecorderContext", () => ({}));