diff --git a/src/components/Pronunciations/Recorder.ts b/src/components/Pronunciations/Recorder.ts index 9985695c1e..a049b387b5 100644 --- a/src/components/Pronunciations/Recorder.ts +++ b/src/components/Pronunciations/Recorder.ts @@ -1,6 +1,9 @@ import RecordRTC from "recordrtc"; -import { getFileNameForWord } from "components/Pronunciations/utilities"; +import { + checkMicPermission, + getFileNameForWord, +} from "components/Pronunciations/utilities"; export default class Recorder { private toast: (textId: string) => void; @@ -63,14 +66,12 @@ export default class Recorder { private onError(err: Error): void { console.error(err); - navigator.permissions - .query({ name: "microphone" as PermissionName }) - .then((result) => { - this.toast( - result.state === "granted" - ? "pronunciations.audioStreamError" - : "pronunciations.noMicAccess" - ); - }); + checkMicPermission().then((hasPermission: boolean) => + this.toast( + hasPermission + ? "pronunciations.audioStreamError" + : "pronunciations.noMicAccess" + ) + ); } } diff --git a/src/components/Pronunciations/RecorderIcon.tsx b/src/components/Pronunciations/RecorderIcon.tsx index c359d74df6..552c3e71d1 100644 --- a/src/components/Pronunciations/RecorderIcon.tsx +++ b/src/components/Pronunciations/RecorderIcon.tsx @@ -8,6 +8,7 @@ import { resetPronunciations, } from "components/Pronunciations/Redux/PronunciationsActions"; import { PronunciationsStatus } from "components/Pronunciations/Redux/PronunciationsReduxTypes"; +import { checkMicPermission } from "components/Pronunciations/utilities"; import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; import { themeColors } from "types/theme"; @@ -37,9 +38,7 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement { const { t } = useTranslation(); useEffect(() => { - navigator.permissions - .query({ name: "microphone" as PermissionName }) - .then((result) => setHasMic(result.state === "granted")); + checkMicPermission().then(setHasMic); }, []); function toggleIsRecordingToTrue(): void { diff --git a/src/components/Pronunciations/utilities.ts b/src/components/Pronunciations/utilities.ts index 2315449a52..eb71348689 100644 --- a/src/components/Pronunciations/utilities.ts +++ b/src/components/Pronunciations/utilities.ts @@ -29,3 +29,26 @@ export async function uploadFileFromPronunciation( URL.revokeObjectURL(fileName); return newId; } + +/** Names of Firefox browsers in user-agent strings, all lowercase. + * + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox */ +const firefoxBrowsers = ["firefox", "focus", "fxios"]; + +/** Check if a user-agent string is of a Firefox browser. */ +function isUserAgentFirefox(userAgent: string): boolean { + const uaLower = userAgent.toLocaleLowerCase(); + return firefoxBrowsers.some((browser) => uaLower.includes(browser)); +} + +/** Checks if the user has granted mic permission to The Combine, + * except on Firefox assumes permission is granted. */ +export async function checkMicPermission(): Promise { + if (!isUserAgentFirefox(navigator.userAgent)) { + const result = await navigator.permissions.query({ + name: "microphone" as PermissionName, // This causes a TypeError on Firefox. + }); + return result.state === "granted"; + } + return true; +}