diff --git a/app/main.ts b/app/main.ts index 47881df1d..853d39bcd 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1862,8 +1862,15 @@ ipc.on( // Permissions Popup-related IPC calls -ipc.on('show-permissions-popup', () => { - showPermissionsPopupWindow(false, false); +ipc.handle('show-permissions-popup', async () => { + try { + await showPermissionsPopupWindow(false, false); + } catch (error) { + getLogger().error( + 'show-permissions-popup error:', + error && error.stack ? error.stack : error + ); + } }); ipc.handle( 'show-calling-permissions-popup', diff --git a/preload.js b/preload.js index c3a1bf1f6..27ecc86fb 100644 --- a/preload.js +++ b/preload.js @@ -239,7 +239,7 @@ try { // Settings-related events window.showSettings = () => ipc.send('show-settings'); - window.showPermissionsPopup = () => ipc.send('show-permissions-popup'); + window.showPermissionsPopup = () => ipc.invoke('show-permissions-popup'); window.showCallingPermissionsPopup = forCamera => ipc.invoke('show-calling-permissions-popup', forCamera); diff --git a/ts/components/conversation/AudioCapture.tsx b/ts/components/conversation/AudioCapture.tsx index 03cf3db07..31861f8f4 100644 --- a/ts/components/conversation/AudioCapture.tsx +++ b/ts/components/conversation/AudioCapture.tsx @@ -97,12 +97,19 @@ export const AudioCapture = ({ const startRecordingShortcut = useStartRecordingShortcut(startRecording); useKeyboardShortcuts(startRecordingShortcut); + const closeToast = useCallback(() => { + setToastType(undefined); + }, []); + // Update timestamp regularly, then timeout if recording goes over five minutes useEffect(() => { if (!isRecording) { return; } + setDurationText(START_DURATION_TEXT); + setToastType(ToastType.VoiceNoteLimit); + const startTime = Date.now(); const interval = setInterval(() => { const duration = moment.duration(Date.now() - startTime, 'ms'); @@ -120,8 +127,15 @@ export const AudioCapture = ({ return () => { clearInterval(interval); + closeToast(); }; - }, [completeRecording, errorRecording, isRecording, setDurationText]); + }, [ + closeToast, + completeRecording, + errorRecording, + isRecording, + setDurationText, + ]); const clickCancel = useCallback(() => { cancelRecording(); @@ -131,10 +145,6 @@ export const AudioCapture = ({ completeRecording(conversationId, onSendAudioRecording); }, [conversationId, completeRecording, onSendAudioRecording]); - const closeToast = useCallback(() => { - setToastType(undefined); - }, []); - let toastElement: JSX.Element | undefined; if (toastType === ToastType.VoiceNoteLimit) { toastElement = ; @@ -226,8 +236,6 @@ export const AudioCapture = ({ if (draftAttachments.length) { setToastType(ToastType.VoiceNoteMustBeOnlyAttachment); } else { - setDurationText(START_DURATION_TEXT); - setToastType(ToastType.VoiceNoteLimit); startRecording(); } }} diff --git a/ts/services/audioRecorder.ts b/ts/services/audioRecorder.ts index 491798c0a..3a8c70dc5 100644 --- a/ts/services/audioRecorder.ts +++ b/ts/services/audioRecorder.ts @@ -1,6 +1,7 @@ // Copyright 2016-2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions'; import * as log from '../logging/log'; import type { WebAudioRecorderClass } from '../window.d'; @@ -42,7 +43,15 @@ export class RecorderClass { } } - async start(): Promise { + async start(): Promise { + const hasMicrophonePermission = await requestMicrophonePermissions(); + if (!hasMicrophonePermission) { + log.info( + 'Recorder/start: Microphone permission was denied, new audio recording not allowed.' + ); + return false; + } + this.clear(); this.context = new AudioContext(); @@ -61,11 +70,11 @@ export class RecorderClass { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); if (!this.context || !this.input) { - this.onError( - this.recorder, - new Error('Recorder/getUserMedia/stream: Missing context or input!') + const err = new Error( + 'Recorder/getUserMedia/stream: Missing context or input!' ); - return; + this.onError(this.recorder, err); + throw err; } this.source = this.context.createMediaStreamSource(stream); this.source.connect(this.input); @@ -81,7 +90,10 @@ export class RecorderClass { if (this.recorder) { this.recorder.startRecording(); + return true; } + + return false; } async stop(): Promise { @@ -120,15 +132,7 @@ export class RecorderClass { this.clear(); - if (error && error.name === 'NotAllowedError') { - log.warn('Recorder/onError: Microphone permission missing'); - window.showPermissionsPopup(); - } else { - log.error( - 'Recorder/onError:', - error && error.stack ? error.stack : error - ); - } + log.error('Recorder/onError:', error && error.stack ? error.stack : error); } getBlob(): Blob { diff --git a/ts/services/calling.ts b/ts/services/calling.ts index f2432ed91..6048900fc 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -92,6 +92,7 @@ import { } from '../calling/constants'; import { callingMessageToProto } from '../util/callingMessageToProto'; import { getSendOptions } from '../util/getSendOptions'; +import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions'; import { SignalService as Proto } from '../protobuf'; import dataInterface from '../sql/Client'; import { @@ -1510,20 +1511,8 @@ export class CallingClass { return true; } - private async requestMicrophonePermissions(): Promise { - const microphonePermission = await window.getMediaPermissions(); - if (!microphonePermission) { - await window.showCallingPermissionsPopup(false); - - // Check the setting again (from the source of truth). - return window.getMediaPermissions(); - } - - return true; - } - private async requestPermissions(isVideoCall: boolean): Promise { - const microphonePermission = await this.requestMicrophonePermissions(); + const microphonePermission = await requestMicrophonePermissions(); if (microphonePermission) { if (isVideoCall) { return this.requestCameraPermissions(); diff --git a/ts/state/ducks/audioRecorder.ts b/ts/state/ducks/audioRecorder.ts index 8c8c47e32..c1fa00f4d 100644 --- a/ts/state/ducks/audioRecorder.ts +++ b/ts/state/ducks/audioRecorder.ts @@ -77,8 +77,10 @@ function startRecording(): ThunkAction< return; } + let recordingStarted = false; + try { - await recorder.start(); + recordingStarted = await recorder.start(); } catch (err) { dispatch({ type: ERROR_RECORDING, @@ -87,10 +89,12 @@ function startRecording(): ThunkAction< return; } - dispatch({ - type: START_RECORDING, - payload: undefined, - }); + if (recordingStarted) { + dispatch({ + type: START_RECORDING, + payload: undefined, + }); + } }; } diff --git a/ts/util/requestMicrophonePermissions.ts b/ts/util/requestMicrophonePermissions.ts new file mode 100644 index 000000000..ce74bee60 --- /dev/null +++ b/ts/util/requestMicrophonePermissions.ts @@ -0,0 +1,14 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export async function requestMicrophonePermissions(): Promise { + const microphonePermission = await window.getMediaPermissions(); + if (!microphonePermission) { + await window.showCallingPermissionsPopup(false); + + // Check the setting again (from the source of truth). + return window.getMediaPermissions(); + } + + return true; +} diff --git a/ts/window.d.ts b/ts/window.d.ts index 93f3672fb..facf4ddb5 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -160,7 +160,7 @@ declare global { QRCode: any; removeSetupMenuItems: () => unknown; - showPermissionsPopup: () => unknown; + showPermissionsPopup: () => Promise; FontFace: typeof FontFace; _: typeof Underscore;