| 
									
										
										
										
											2023-01-03 11:55:46 -08:00
										 |  |  | // Copyright 2016 Signal Messenger, LLC
 | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | // SPDX-License-Identifier: AGPL-3.0-only
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-15 14:51:58 -04:00
										 |  |  | import React, { useCallback, useEffect, useState } from 'react'; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | import * as moment from 'moment'; | 
					
						
							|  |  |  | import { noop } from 'lodash'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 13:54:33 -08:00
										 |  |  | import type { | 
					
						
							|  |  |  |   AttachmentDraftType, | 
					
						
							|  |  |  |   InMemoryAttachmentDraftType, | 
					
						
							|  |  |  | } from '../../types/Attachment'; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | import { ConfirmationDialog } from '../ConfirmationDialog'; | 
					
						
							| 
									
										
										
										
											2021-10-26 14:15:33 -05:00
										 |  |  | import type { LocalizerType } from '../../types/Util'; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | import { ToastVoiceNoteLimit } from '../ToastVoiceNoteLimit'; | 
					
						
							|  |  |  | import { ToastVoiceNoteMustBeOnlyAttachment } from '../ToastVoiceNoteMustBeOnlyAttachment'; | 
					
						
							|  |  |  | import { useEscapeHandling } from '../../hooks/useEscapeHandling'; | 
					
						
							|  |  |  | import { | 
					
						
							| 
									
										
										
										
											2021-10-15 14:51:58 -04:00
										 |  |  |   useStartRecordingShortcut, | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   useKeyboardShortcuts, | 
					
						
							|  |  |  | } from '../../hooks/useKeyboardShortcuts'; | 
					
						
							| 
									
										
										
										
											2023-01-20 19:31:30 -05:00
										 |  |  | import { | 
					
						
							|  |  |  |   ErrorDialogAudioRecorderType, | 
					
						
							|  |  |  |   RecordingState, | 
					
						
							|  |  |  | } from '../../types/AudioRecorder'; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 13:54:33 -08:00
										 |  |  | type OnSendAudioRecordingType = (rec: InMemoryAttachmentDraftType) => unknown; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | export type PropsType = { | 
					
						
							|  |  |  |   cancelRecording: () => unknown; | 
					
						
							|  |  |  |   conversationId: string; | 
					
						
							|  |  |  |   completeRecording: ( | 
					
						
							|  |  |  |     conversationId: string, | 
					
						
							|  |  |  |     onSendAudioRecording?: OnSendAudioRecordingType | 
					
						
							|  |  |  |   ) => unknown; | 
					
						
							| 
									
										
										
										
											2021-11-15 13:54:33 -08:00
										 |  |  |   draftAttachments: ReadonlyArray<AttachmentDraftType>; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType; | 
					
						
							|  |  |  |   errorRecording: (e: ErrorDialogAudioRecorderType) => unknown; | 
					
						
							|  |  |  |   i18n: LocalizerType; | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |   recordingState: RecordingState; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   onSendAudioRecording: OnSendAudioRecordingType; | 
					
						
							| 
									
										
										
										
											2023-01-04 19:22:36 -05:00
										 |  |  |   startRecording: (id: string) => unknown; | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | enum ToastType { | 
					
						
							|  |  |  |   VoiceNoteLimit, | 
					
						
							|  |  |  |   VoiceNoteMustBeOnlyAttachment, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const START_DURATION_TEXT = '0:00'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-17 16:45:19 -08:00
										 |  |  | export function AudioCapture({ | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   cancelRecording, | 
					
						
							|  |  |  |   completeRecording, | 
					
						
							|  |  |  |   conversationId, | 
					
						
							|  |  |  |   draftAttachments, | 
					
						
							|  |  |  |   errorDialogAudioRecorderType, | 
					
						
							|  |  |  |   errorRecording, | 
					
						
							|  |  |  |   i18n, | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |   recordingState, | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   onSendAudioRecording, | 
					
						
							|  |  |  |   startRecording, | 
					
						
							| 
									
										
										
										
											2022-11-17 16:45:19 -08:00
										 |  |  | }: PropsType): JSX.Element { | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   const [durationText, setDurationText] = useState<string>(START_DURATION_TEXT); | 
					
						
							|  |  |  |   const [toastType, setToastType] = useState<ToastType | undefined>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Cancel recording if we switch away from this conversation, unmounting
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     return () => { | 
					
						
							|  |  |  |       cancelRecording(); | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |   }, [cancelRecording]); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // Stop recording and show confirmation if user switches away from this app
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |     if (recordingState !== RecordingState.Recording) { | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const handler = () => { | 
					
						
							|  |  |  |       errorRecording(ErrorDialogAudioRecorderType.Blur); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     window.addEventListener('blur', handler); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return () => { | 
					
						
							|  |  |  |       window.removeEventListener('blur', handler); | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |   }, [recordingState, completeRecording, errorRecording]); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const escapeRecording = useCallback(() => { | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |     if (recordingState !== RecordingState.Recording) { | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     cancelRecording(); | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |   }, [cancelRecording, recordingState]); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |   useEscapeHandling(escapeRecording); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-04 19:22:36 -05:00
										 |  |  |   const recordConversation = useCallback( | 
					
						
							|  |  |  |     () => startRecording(conversationId), | 
					
						
							|  |  |  |     [conversationId, startRecording] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  |   const startRecordingShortcut = useStartRecordingShortcut(recordConversation); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   useKeyboardShortcuts(startRecordingShortcut); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-27 11:16:09 -05:00
										 |  |  |   const closeToast = useCallback(() => { | 
					
						
							|  |  |  |     setToastType(undefined); | 
					
						
							|  |  |  |   }, []); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   // Update timestamp regularly, then timeout if recording goes over five minutes
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |     if (recordingState !== RecordingState.Recording) { | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-27 11:16:09 -05:00
										 |  |  |     setDurationText(START_DURATION_TEXT); | 
					
						
							|  |  |  |     setToastType(ToastType.VoiceNoteLimit); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |     const startTime = Date.now(); | 
					
						
							|  |  |  |     const interval = setInterval(() => { | 
					
						
							|  |  |  |       const duration = moment.duration(Date.now() - startTime, 'ms'); | 
					
						
							|  |  |  |       const minutes = `${Math.trunc(duration.asMinutes())}`; | 
					
						
							|  |  |  |       let seconds = `${duration.seconds()}`; | 
					
						
							|  |  |  |       if (seconds.length < 2) { | 
					
						
							|  |  |  |         seconds = `0${seconds}`; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       setDurationText(`${minutes}:${seconds}`); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-06 14:39:54 -07:00
										 |  |  |       if (duration >= moment.duration(1, 'hours')) { | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |         errorRecording(ErrorDialogAudioRecorderType.Timeout); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, 1000); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return () => { | 
					
						
							|  |  |  |       clearInterval(interval); | 
					
						
							| 
									
										
										
										
											2021-10-27 11:16:09 -05:00
										 |  |  |       closeToast(); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |     }; | 
					
						
							| 
									
										
										
										
											2021-10-27 11:16:09 -05:00
										 |  |  |   }, [ | 
					
						
							|  |  |  |     closeToast, | 
					
						
							|  |  |  |     completeRecording, | 
					
						
							|  |  |  |     errorRecording, | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |     recordingState, | 
					
						
							| 
									
										
										
										
											2021-10-27 11:16:09 -05:00
										 |  |  |     setDurationText, | 
					
						
							|  |  |  |   ]); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const clickCancel = useCallback(() => { | 
					
						
							|  |  |  |     cancelRecording(); | 
					
						
							|  |  |  |   }, [cancelRecording]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const clickSend = useCallback(() => { | 
					
						
							|  |  |  |     completeRecording(conversationId, onSendAudioRecording); | 
					
						
							|  |  |  |   }, [conversationId, completeRecording, onSendAudioRecording]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let toastElement: JSX.Element | undefined; | 
					
						
							|  |  |  |   if (toastType === ToastType.VoiceNoteLimit) { | 
					
						
							|  |  |  |     toastElement = <ToastVoiceNoteLimit i18n={i18n} onClose={closeToast} />; | 
					
						
							|  |  |  |   } else if (toastType === ToastType.VoiceNoteMustBeOnlyAttachment) { | 
					
						
							|  |  |  |     toastElement = ( | 
					
						
							|  |  |  |       <ToastVoiceNoteMustBeOnlyAttachment i18n={i18n} onClose={closeToast} /> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 16:13:47 -04:00
										 |  |  |   let confirmationDialog: JSX.Element | undefined; | 
					
						
							|  |  |  |   if ( | 
					
						
							|  |  |  |     errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.Blur || | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |     errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.Timeout | 
					
						
							|  |  |  |   ) { | 
					
						
							| 
									
										
										
										
											2021-09-30 16:13:47 -04:00
										 |  |  |     const confirmationDialogText = | 
					
						
							|  |  |  |       errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.Blur | 
					
						
							|  |  |  |         ? i18n('voiceRecordingInterruptedBlur') | 
					
						
							|  |  |  |         : i18n('voiceRecordingInterruptedMax'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     confirmationDialog = ( | 
					
						
							|  |  |  |       <ConfirmationDialog | 
					
						
							| 
									
										
										
										
											2022-09-27 13:24:21 -07:00
										 |  |  |         dialogName="AudioCapture.sendAnyway" | 
					
						
							| 
									
										
										
										
											2021-09-30 16:13:47 -04:00
										 |  |  |         i18n={i18n} | 
					
						
							|  |  |  |         onCancel={clickCancel} | 
					
						
							|  |  |  |         onClose={noop} | 
					
						
							|  |  |  |         cancelText={i18n('discard')} | 
					
						
							|  |  |  |         actions={[ | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             text: i18n('sendAnyway'), | 
					
						
							|  |  |  |             style: 'affirmative', | 
					
						
							|  |  |  |             action: clickSend, | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         ]} | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         {confirmationDialogText} | 
					
						
							|  |  |  |       </ConfirmationDialog> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } else if ( | 
					
						
							|  |  |  |     errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.ErrorRecording | 
					
						
							|  |  |  |   ) { | 
					
						
							|  |  |  |     confirmationDialog = ( | 
					
						
							|  |  |  |       <ConfirmationDialog | 
					
						
							| 
									
										
										
										
											2022-09-27 13:24:21 -07:00
										 |  |  |         dialogName="AudioCapture.error" | 
					
						
							| 
									
										
										
										
											2021-09-30 16:13:47 -04:00
										 |  |  |         i18n={i18n} | 
					
						
							|  |  |  |         onCancel={clickCancel} | 
					
						
							|  |  |  |         onClose={noop} | 
					
						
							|  |  |  |         cancelText={i18n('ok')} | 
					
						
							|  |  |  |         actions={[]} | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         {i18n('voiceNoteError')} | 
					
						
							|  |  |  |       </ConfirmationDialog> | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-11 15:33:35 -08:00
										 |  |  |   if (recordingState === RecordingState.Recording && !confirmationDialog) { | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |     return ( | 
					
						
							|  |  |  |       <> | 
					
						
							|  |  |  |         <div className="AudioCapture"> | 
					
						
							|  |  |  |           <button | 
					
						
							|  |  |  |             className="AudioCapture__recorder-button AudioCapture__recorder-button--complete" | 
					
						
							|  |  |  |             onClick={clickSend} | 
					
						
							|  |  |  |             tabIndex={0} | 
					
						
							|  |  |  |             title={i18n('voiceRecording--complete')} | 
					
						
							|  |  |  |             type="button" | 
					
						
							|  |  |  |           > | 
					
						
							|  |  |  |             <span className="icon" /> | 
					
						
							|  |  |  |           </button> | 
					
						
							|  |  |  |           <span className="AudioCapture__time">{durationText}</span> | 
					
						
							|  |  |  |           <button | 
					
						
							|  |  |  |             className="AudioCapture__recorder-button AudioCapture__recorder-button--cancel" | 
					
						
							|  |  |  |             onClick={clickCancel} | 
					
						
							|  |  |  |             tabIndex={0} | 
					
						
							|  |  |  |             title={i18n('voiceRecording--cancel')} | 
					
						
							|  |  |  |             type="button" | 
					
						
							|  |  |  |           > | 
					
						
							|  |  |  |             <span className="icon" /> | 
					
						
							|  |  |  |           </button> | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |         {toastElement} | 
					
						
							|  |  |  |       </> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <> | 
					
						
							|  |  |  |       <div className="AudioCapture"> | 
					
						
							|  |  |  |         <button | 
					
						
							|  |  |  |           aria-label={i18n('voiceRecording--start')} | 
					
						
							|  |  |  |           className="AudioCapture__microphone" | 
					
						
							|  |  |  |           onClick={() => { | 
					
						
							|  |  |  |             if (draftAttachments.length) { | 
					
						
							|  |  |  |               setToastType(ToastType.VoiceNoteMustBeOnlyAttachment); | 
					
						
							|  |  |  |             } else { | 
					
						
							| 
									
										
										
										
											2023-01-04 19:22:36 -05:00
										 |  |  |               startRecording(conversationId); | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |             } | 
					
						
							|  |  |  |           }} | 
					
						
							|  |  |  |           title={i18n('voiceRecording--start')} | 
					
						
							|  |  |  |           type="button" | 
					
						
							|  |  |  |         /> | 
					
						
							| 
									
										
										
										
											2021-09-30 16:13:47 -04:00
										 |  |  |         {confirmationDialog} | 
					
						
							| 
									
										
										
										
											2021-09-29 16:23:06 -04:00
										 |  |  |       </div> | 
					
						
							|  |  |  |       {toastElement} | 
					
						
							|  |  |  |     </> | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2022-11-17 16:45:19 -08:00
										 |  |  | } |