From 6836be879a36b06cdac4cca24436ae0b98e58914 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 29 Jul 2022 12:10:48 -0300 Subject: [PATCH 1/4] Migrate audio recording to TypeScript --- .../messageBox/messageBoxAudioMessage.js | 10 +- apps/meteor/app/ui/client/index.ts | 2 +- .../{audioEncoder.js => AudioEncoder.ts} | 21 ++-- .../ui/client/lib/recorderjs/AudioRecorder.ts | 90 +++++++++++++++ .../ui/client/lib/recorderjs/audioRecorder.js | 105 ------------------ 5 files changed, 105 insertions(+), 123 deletions(-) rename apps/meteor/app/ui/client/lib/recorderjs/{audioEncoder.js => AudioEncoder.ts} (72%) create mode 100644 apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts delete mode 100644 apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js index e1a6c425cbcf..055063970279 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js @@ -2,13 +2,15 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { settings } from '../../../settings'; -import { AudioRecorder, fileUpload, USER_ACTIVITIES, UserAction } from '../../../ui'; +import { AudioRecorder, fileUpload, USER_ACTIVITIES, UserAction } from '../../../ui/client'; import { t } from '../../../utils'; import './messageBoxAudioMessage.html'; +const audioRecorder = new AudioRecorder(); + const startRecording = async (rid, tmid) => { try { - await AudioRecorder.start(); + await audioRecorder.start(); UserAction.performContinuously(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); } catch (error) { throw error; @@ -16,7 +18,7 @@ const startRecording = async (rid, tmid) => { }; const stopRecording = async (rid, tmid) => { - const result = await new Promise((resolve) => AudioRecorder.stop(resolve)); + const result = await new Promise((resolve) => audioRecorder.stop(resolve)); UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); return result; }; @@ -87,7 +89,7 @@ Template.messageBoxAudioMessage.onDestroyed(async function () { Template.messageBoxAudioMessage.helpers({ isAllowed() { return ( - AudioRecorder.isSupported() && + audioRecorder.isSupported() && !Template.instance().isMicrophoneDenied.get() && settings.get('FileUpload_Enabled') && settings.get('Message_AudioRecorderEnabled') && diff --git a/apps/meteor/app/ui/client/index.ts b/apps/meteor/app/ui/client/index.ts index 2a16ec295fa7..08c51e31aae4 100644 --- a/apps/meteor/app/ui/client/index.ts +++ b/apps/meteor/app/ui/client/index.ts @@ -30,7 +30,7 @@ export { fileUpload } from './lib/fileUpload'; export { UserAction, USER_ACTIVITIES } from './lib/UserAction'; export { KonchatNotification } from './lib/notification'; export { Login, Button } from './lib/rocket'; -export { AudioRecorder } from './lib/recorderjs/audioRecorder'; +export { AudioRecorder } from './lib/recorderjs/AudioRecorder'; export { VideoRecorder } from './lib/recorderjs/videoRecorder'; export { chatMessages } from './views/app/room'; export * from './lib/userPopoverStatus'; diff --git a/apps/meteor/app/ui/client/lib/recorderjs/audioEncoder.js b/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts similarity index 72% rename from apps/meteor/app/ui/client/lib/recorderjs/audioEncoder.js rename to apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts index 81934c69c191..7f7dcb169db4 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/audioEncoder.js +++ b/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts @@ -1,10 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { Emitter } from '@rocket.chat/emitter'; -import { settings } from '../../../../settings'; +export class AudioEncoder extends Emitter { + private worker: Worker; -class AudioEncoder extends Emitter { - constructor(source, { bufferLen = 4096, numChannels = 1, bitRate = settings.get('Message_Audio_bitRate') || 32 } = {}) { + private scriptNode: ScriptProcessorNode; + + constructor(source: MediaStreamAudioSourceNode, { bufferLen = 4096, numChannels = 1, bitRate = 32 } = {}) { super(); const workerPath = Meteor.absoluteUrl('workers/mp3-encoder/index.js'); @@ -21,12 +23,7 @@ class AudioEncoder extends Emitter { }, }); - this.scriptNode = (source.context.createScriptProcessor || source.context.createJavaScriptNode).call( - source.context, - bufferLen, - numChannels, - numChannels, - ); + this.scriptNode = source.context.createScriptProcessor(bufferLen, numChannels, numChannels); this.scriptNode.onaudioprocess = this.handleAudioProcess; source.connect(this.scriptNode); @@ -37,7 +34,7 @@ class AudioEncoder extends Emitter { this.worker.postMessage({ command: 'finish' }); } - handleWorkerMessage = (event) => { + handleWorkerMessage = (event: MessageEvent) => { switch (event.data.command) { case 'end': { // prepend mp3 magic number to the buffer @@ -51,7 +48,7 @@ class AudioEncoder extends Emitter { } }; - handleAudioProcess = (event) => { + handleAudioProcess = (event: AudioProcessingEvent) => { for (let channel = 0; channel < event.inputBuffer.numberOfChannels; channel++) { const buffer = event.inputBuffer.getChannelData(channel); this.worker.postMessage({ @@ -61,5 +58,3 @@ class AudioEncoder extends Emitter { } }; } - -export { AudioEncoder }; diff --git a/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts new file mode 100644 index 000000000000..74e41826c8a1 --- /dev/null +++ b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts @@ -0,0 +1,90 @@ +import { AudioEncoder } from './AudioEncoder'; +import { settings } from '../../../../settings/client'; + +export class AudioRecorder { + private audioContext: AudioContext | undefined; + + private stream: MediaStream | undefined; + + private encoder: AudioEncoder | undefined; + + isSupported() { + return Boolean(navigator.mediaDevices?.getUserMedia) && Boolean(AudioContext); + } + + createAudioContext() { + if (this.audioContext) { + return; + } + + this.audioContext = new AudioContext(); + } + + destroyAudioContext() { + if (!this.audioContext) { + return; + } + + this.audioContext.close(); + delete this.audioContext; + } + + async createStream() { + if (this.stream) { + return; + } + + this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + } + + destroyStream() { + if (!this.stream) { + return; + } + + this.stream.getAudioTracks().forEach((track) => track.stop()); + delete this.stream; + } + + async createEncoder() { + if (this.encoder || !this.audioContext || !this.stream) { + return; + } + + const input = this.audioContext?.createMediaStreamSource(this.stream); + this.encoder = new AudioEncoder(input, { bitRate: settings.get('Message_Audio_bitRate') || 32 }); + } + + destroyEncoder() { + if (!this.encoder) { + return; + } + + this.encoder.close(); + delete this.encoder; + } + + async start(cb: (this: this, done: boolean) => void) { + try { + this.createAudioContext(); + await this.createStream(); + await this.createEncoder(); + cb?.call(this, true); + } catch (error) { + console.error(error); + this.destroyEncoder(); + this.destroyStream(); + this.destroyAudioContext(); + cb?.call(this, false); + } + } + + stop(cb: () => void) { + this.encoder?.on('encoded', cb); + this.encoder?.close(); + + this.destroyEncoder(); + this.destroyStream(); + this.destroyAudioContext(); + } +} diff --git a/apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js b/apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js deleted file mode 100644 index 51f83a2091ef..000000000000 --- a/apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js +++ /dev/null @@ -1,105 +0,0 @@ -import { AudioEncoder } from './audioEncoder'; - -const getUserMedia = ((navigator) => { - if (navigator.mediaDevices) { - return navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); - } - - const legacyGetUserMedia = - navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; - - if (legacyGetUserMedia) { - return (options) => - new Promise((resolve, reject) => { - legacyGetUserMedia.call(navigator, options, resolve, reject); - }); - } -})(window.navigator); - -const AudioContext = window.AudioContext || window.webkitAudioContext; - -class AudioRecorder { - isSupported() { - return Boolean(getUserMedia) && Boolean(AudioContext); - } - - createAudioContext() { - if (this.audioContext) { - return; - } - - this.audioContext = new AudioContext(); - } - - destroyAudioContext() { - if (!this.audioContext) { - return; - } - - this.audioContext.close(); - delete this.audioContext; - } - - async createStream() { - if (this.stream) { - return; - } - - this.stream = await getUserMedia({ audio: true }); - } - - destroyStream() { - if (!this.stream) { - return; - } - - this.stream.getAudioTracks().forEach((track) => track.stop()); - delete this.stream; - } - - async createEncoder() { - if (this.encoder) { - return; - } - - const input = this.audioContext.createMediaStreamSource(this.stream); - this.encoder = new AudioEncoder(input); - } - - destroyEncoder() { - if (!this.encoder) { - return; - } - - this.encoder.close(); - delete this.encoder; - } - - async start(cb) { - try { - await this.createAudioContext(); - await this.createStream(); - await this.createEncoder(); - cb && cb.call(this, true); - } catch (error) { - console.error(error); - this.destroyEncoder(); - this.destroyStream(); - this.destroyAudioContext(); - cb && cb.call(this, false); - } - } - - stop(cb) { - this.encoder.on('encoded', cb); - this.encoder.close(); - - this.destroyEncoder(); - this.destroyStream(); - this.destroyAudioContext(); - } -} - -const instance = new AudioRecorder(); - -export { instance as AudioRecorder }; From 22e8afc85cd8046a2dac71e691242a63487a060e Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 29 Jul 2022 17:02:59 -0300 Subject: [PATCH 2/4] Migrate template to component --- .../client/imports/components/message-box.css | 9 +- .../client/messageBox/messageBox.html | 4 +- .../client/messageBox/messageBox.js | 1 - .../messageBox/messageBoxAudioMessage.html | 22 --- .../messageBox/messageBoxAudioMessage.js | 164 --------------- apps/meteor/app/ui/client/lib/fileUpload.ts | 2 +- .../ui/client/lib/recorderjs/AudioRecorder.ts | 4 +- apps/meteor/client/templates.ts | 2 + .../AudioMessageRecorder.tsx | 186 ++++++++++++++++++ .../composer/AudioMessageRecorder/index.ts | 1 + .../rocketchat-i18n/i18n/en.i18n.json | 3 +- 11 files changed, 197 insertions(+), 201 deletions(-) delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js create mode 100644 apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx create mode 100644 apps/meteor/client/views/composer/AudioMessageRecorder/index.ts diff --git a/apps/meteor/app/theme/client/imports/components/message-box.css b/apps/meteor/app/theme/client/imports/components/message-box.css index c775af9fa865..dcf3242123aa 100644 --- a/apps/meteor/app/theme/client/imports/components/message-box.css +++ b/apps/meteor/app/theme/client/imports/components/message-box.css @@ -171,8 +171,7 @@ &-done, &-cancel, - &-timer, - &-loading { + &-timer { display: none; } @@ -209,12 +208,6 @@ } } - &-loading { - cursor: pointer; - - animation: spin 1s linear infinite; - } - &--recording { .rc-message-box__audio-message-mic, .rc-message-box__audio-message-loading { diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.html b/apps/meteor/app/ui-message/client/messageBox/messageBox.html index ad15ba0d9b39..ace9e3fc6978 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.html @@ -26,7 +26,7 @@ {{else}}
{{/unless}} - +
{{#if isSendIconVisible}} @@ -39,7 +39,7 @@ {{ else }} {{#unless isFederatedRoom}} {{#if canSend}} - {{> messageBoxAudioMessage rid=rid tmid=tmid}} + {{> AudioMessageRecorder rid=rid tmid=tmid }} {{#if actions}} diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.js b/apps/meteor/app/ui-message/client/messageBox/messageBox.js index 6ed02c7e7b6c..30977e093ba3 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.js +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.js @@ -18,7 +18,6 @@ import { t, getUserPreference } from '../../../utils/client'; import './messageBoxActions'; import './messageBoxReplyPreview'; import './userActionIndicator.ts'; -import './messageBoxAudioMessage'; import './messageBoxNotSubscribed'; import './messageBox.html'; import './messageBoxReadOnly'; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html deleted file mode 100644 index 806ea08ca380..000000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js deleted file mode 100644 index 055063970279..000000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.js +++ /dev/null @@ -1,164 +0,0 @@ -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; - -import { settings } from '../../../settings'; -import { AudioRecorder, fileUpload, USER_ACTIVITIES, UserAction } from '../../../ui/client'; -import { t } from '../../../utils'; -import './messageBoxAudioMessage.html'; - -const audioRecorder = new AudioRecorder(); - -const startRecording = async (rid, tmid) => { - try { - await audioRecorder.start(); - UserAction.performContinuously(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); - } catch (error) { - throw error; - } -}; - -const stopRecording = async (rid, tmid) => { - const result = await new Promise((resolve) => audioRecorder.stop(resolve)); - UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); - return result; -}; - -const recordingInterval = new ReactiveVar(null); -const recordingRoomId = new ReactiveVar(null); - -const clearIntervalVariables = () => { - if (recordingInterval.get()) { - clearInterval(recordingInterval.get()); - recordingInterval.set(null); - recordingRoomId.set(null); - } -}; - -const cancelRecording = async (instance, rid, tmid) => { - clearIntervalVariables(); - - instance.time.set('00:00'); - - const blob = await stopRecording(rid, tmid); - - instance.state.set(null); - - return blob; -}; - -Template.messageBoxAudioMessage.onCreated(async function () { - this.state = new ReactiveVar(null); - this.time = new ReactiveVar('00:00'); - this.isMicrophoneDenied = new ReactiveVar(false); - - if (navigator.permissions) { - try { - const permissionStatus = await navigator.permissions.query({ name: 'microphone' }); - this.isMicrophoneDenied.set(permissionStatus.state === 'denied'); - permissionStatus.onchange = () => { - this.isMicrophoneDenied.set(permissionStatus.state === 'denied'); - }; - return; - } catch (error) { - console.warn(error); - } - } - - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - this.isMicrophoneDenied.set(true); - return; - } - - try { - if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { - this.isMicrophoneDenied.set(true); - return; - } - } catch (error) { - console.warn(error); - } -}); - -Template.messageBoxAudioMessage.onDestroyed(async function () { - if (this.state.get() === 'recording') { - const { rid, tmid } = this.data; - await cancelRecording(this, rid, tmid); - } -}); - -Template.messageBoxAudioMessage.helpers({ - isAllowed() { - return ( - audioRecorder.isSupported() && - !Template.instance().isMicrophoneDenied.get() && - settings.get('FileUpload_Enabled') && - settings.get('Message_AudioRecorderEnabled') && - (!settings.get('FileUpload_MediaTypeBlackList') || !settings.get('FileUpload_MediaTypeBlackList').match(/audio\/mp3|audio\/\*/i)) && - (!settings.get('FileUpload_MediaTypeWhiteList') || settings.get('FileUpload_MediaTypeWhiteList').match(/audio\/mp3|audio\/\*/i)) - ); - }, - - stateClass() { - if (recordingRoomId.get() && recordingRoomId.get() !== Template.currentData().rid) { - return 'rc-message-box__audio-message--busy'; - } - - const state = Template.instance().state.get(); - return state && `rc-message-box__audio-message--${state}`; - }, - - time() { - return Template.instance().time.get(); - }, -}); - -Template.messageBoxAudioMessage.events({ - async 'click .js-audio-message-record'(event, instance) { - event.preventDefault(); - - if (recordingRoomId.get() && recordingRoomId.get() !== this.rid) { - return; - } - - instance.state.set('recording'); - - try { - await startRecording(this.rid, this.tmid); - const startTime = new Date(); - recordingInterval.set( - setInterval(() => { - const now = new Date(); - const distance = (now.getTime() - startTime.getTime()) / 1000; - const minutes = Math.floor(distance / 60); - const seconds = Math.floor(distance % 60); - instance.time.set(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`); - }, 1000), - ); - recordingRoomId.set(this.rid); - } catch (error) { - console.log(error); - instance.isMicrophoneDenied.set(true); - instance.state.set(null); - } - }, - - async 'click .js-audio-message-cancel'(event, instance) { - event.preventDefault(); - - await cancelRecording(instance, this.rid, this.tmid); - }, - - async 'click .js-audio-message-done'(event, instance) { - event.preventDefault(); - - instance.state.set('loading'); - - const { rid, tmid } = this; - const blob = await cancelRecording(instance, rid, tmid); - - const fileName = `${t('Audio record')}.mp3`; - const file = new File([blob], fileName, { type: 'audio/mpeg' }); - - await fileUpload([{ file, type: 'audio/mpeg', name: fileName }], { input: blob }, { rid, tmid }); - }, -}); diff --git a/apps/meteor/app/ui/client/lib/fileUpload.ts b/apps/meteor/app/ui/client/lib/fileUpload.ts index 168497e522e4..4650f2f64ff6 100644 --- a/apps/meteor/app/ui/client/lib/fileUpload.ts +++ b/apps/meteor/app/ui/client/lib/fileUpload.ts @@ -154,7 +154,7 @@ type FileUploadProp = SingleOrArray<{ /* @deprecated */ export const fileUpload = async ( f: FileUploadProp, - input: HTMLInputElement, + input: HTMLInputElement | ArrayLike, { rid, tmid, diff --git a/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts index 74e41826c8a1..c4b550359652 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts +++ b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts @@ -64,7 +64,7 @@ export class AudioRecorder { delete this.encoder; } - async start(cb: (this: this, done: boolean) => void) { + async start(cb?: (this: this, done: boolean) => void) { try { this.createAudioContext(); await this.createStream(); @@ -79,7 +79,7 @@ export class AudioRecorder { } } - stop(cb: () => void) { + stop(cb: (data: Blob) => void) { this.encoder?.on('encoded', cb); this.encoder?.close(); diff --git a/apps/meteor/client/templates.ts b/apps/meteor/client/templates.ts index 3af359e5dc59..61999524e155 100644 --- a/apps/meteor/client/templates.ts +++ b/apps/meteor/client/templates.ts @@ -159,3 +159,5 @@ createTemplateForComponent('ComposerNotAvailablePhoneCalls', () => import('./com createTemplateForComponent('loggedOutBanner', () => import('../ee/client/components/deviceManagement/LoggedOutBanner'), { renderContainerView: () => HTML.DIV({ style: 'max-width: 520px; margin: 0 auto;' }), }); + +createTemplateForComponent('AudioMessageRecorder', () => import('./views/composer/AudioMessageRecorder')); diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx new file mode 100644 index 000000000000..861bdbcea7da --- /dev/null +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx @@ -0,0 +1,186 @@ +import { IMessage, IRoom } from '@rocket.chat/core-typings'; +import { Icon, Throbber } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useEffect, useMemo, useState } from 'react'; + +import { AudioRecorder, fileUpload, UserAction, USER_ACTIVITIES } from '../../../../app/ui/client'; + +const audioRecorder = new AudioRecorder(); + +type AudioMessageRecorderProps = { + rid: IRoom['_id']; + tmid: IMessage['_id']; +}; + +const AudioMessageRecorder = ({ rid, tmid }: AudioMessageRecorderProps): ReactElement | null => { + const t = useTranslation(); + + const [state, setState] = useState<'idle' | 'loading' | 'recording'>('idle'); + const [time, setTime] = useState('00:00'); + const [isMicrophoneDenied, setIsMicrophoneDenied] = useState(false); + const [recordingInterval, setRecordingInterval] = useState | null>(null); + const [recordingRoomId, setRecordingRoomId] = useState(null); + + const stopRecording = useMutableCallback(async () => { + if (recordingInterval) { + clearInterval(recordingInterval ?? undefined); + } + setRecordingInterval(null); + setRecordingRoomId(null); + + setTime('00:00'); + + const blob = await new Promise((resolve) => audioRecorder.stop(resolve)); + UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); + + setState('idle'); + + return blob; + }); + + const handleMount = useMutableCallback(async (): Promise => { + if (navigator.permissions) { + try { + const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName }); + setIsMicrophoneDenied(permissionStatus.state === 'denied'); + permissionStatus.onchange = (): void => { + setIsMicrophoneDenied(permissionStatus.state === 'denied'); + }; + return; + } catch (error) { + console.warn(error); + } + } + + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + setIsMicrophoneDenied(true); + return; + } + + try { + if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { + setIsMicrophoneDenied(true); + return; + } + } catch (error) { + console.warn(error); + } + }); + + const handleUnmount = useMutableCallback(async () => { + if (state === 'recording') { + await stopRecording(); + } + }); + + useEffect(() => { + handleMount(); + + return () => { + handleUnmount(); + }; + }, [handleMount, handleUnmount]); + + const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; + const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean; + const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; + const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; + + const isAllowed = useMemo( + () => + audioRecorder.isSupported() && + !isMicrophoneDenied && + isFileUploadEnabled && + isAudioRecorderEnabled && + (!fileUploadMediaTypeBlackList || !fileUploadMediaTypeBlackList.match(/audio\/mp3|audio\/\*/i)) && + (!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/audio\/mp3|audio\/\*/i)), + [fileUploadMediaTypeBlackList, fileUploadMediaTypeWhiteList, isAudioRecorderEnabled, isFileUploadEnabled, isMicrophoneDenied], + ); + + const stateClass = useMemo(() => { + if (recordingRoomId && recordingRoomId !== rid) { + return 'rc-message-box__audio-message--busy'; + } + + return state && `rc-message-box__audio-message--${state}`; + }, [recordingRoomId, rid, state]); + + const handleRecordButtonClick = useMutableCallback(async () => { + if (recordingRoomId && recordingRoomId !== rid) { + return; + } + + setState('recording'); + + try { + await audioRecorder.start(); + UserAction.performContinuously(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); + const startTime = new Date(); + setRecordingInterval( + setInterval(() => { + const now = new Date(); + const distance = (now.getTime() - startTime.getTime()) / 1000; + const minutes = Math.floor(distance / 60); + const seconds = Math.floor(distance % 60); + setTime(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`); + }, 1000), + ); + setRecordingRoomId(rid); + } catch (error) { + console.log(error); + setIsMicrophoneDenied(true); + setState('idle'); + } + }); + + const handleCancelButtonClick = useMutableCallback(async () => { + await stopRecording(); + }); + + const handleDoneButtonClick = useMutableCallback(async () => { + setState('loading'); + + const blob = await stopRecording(); + + const fileName = `${t('Audio_record')}.mp3`; + const file = new File([blob], fileName, { type: 'audio/mpeg' }); + + await fileUpload([{ file, name: fileName }], [], { rid, tmid }); + }); + + if (!isAllowed) { + return null; + } + + return ( +
+ {state === 'recording' && ( + <> +
+ +
+
+ + {time} +
+
+ +
+ + )} + {state === 'idle' && ( +
+ +
+ )} + {state === 'loading' && ( +
+ +
+ )} +
+ ); +}; + +export default AudioMessageRecorder; diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/index.ts b/apps/meteor/client/views/composer/AudioMessageRecorder/index.ts new file mode 100644 index 000000000000..a18f21430c51 --- /dev/null +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/index.ts @@ -0,0 +1 @@ +export { default } from './AudioMessageRecorder'; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index ccbd2edfcb2d..ee164b31c2eb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -599,6 +599,7 @@ "Audio_Notification_Value_Description": "Can be any custom sound or the default ones: beep, chelle, ding, droplet, highbell, seasons", "Audio_Notifications_Default_Alert": "Audio Notifications Default Alert", "Audio_Notifications_Value": "Default Message Notification Audio", + "Audio_record": "Audio record", "Audios": "Audios", "Auditing": "Auditing", "Auth": "Auth", @@ -5245,4 +5246,4 @@ "Device_Management_Email_Subject": "[Site_Name] - Login Detected", "Device_Management_Email_Body": "You may use the following placeholders:

{Login_Detected}

[name] ([username]) {Logged_In_Via}

{Device_Management_Client}: [browserInfo]
{Device_Management_OS}: [osInfo]
{Device_Management_Device}: [deviceInfo]
{Device_Management_IP}:[ipInfo]

[userAgent]

{Access_Your_Account}

{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}
[SITE_URL]

{Thank_You_For_Choosing_RocketChat}

", "Something_Went_Wrong": "Something went wrong" -} \ No newline at end of file +} From 1a7d6581762eeaa6dc892031cf3a9661e2d0f5bb Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 4 Oct 2022 15:52:27 -0300 Subject: [PATCH 3/4] LGTM --- .../composer/AudioMessageRecorder/AudioMessageRecorder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx index 861bdbcea7da..5d9e8763bc73 100644 --- a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx @@ -24,7 +24,7 @@ const AudioMessageRecorder = ({ rid, tmid }: AudioMessageRecorderProps): ReactEl const stopRecording = useMutableCallback(async () => { if (recordingInterval) { - clearInterval(recordingInterval ?? undefined); + clearInterval(recordingInterval); } setRecordingInterval(null); setRecordingRoomId(null); From cf58ce1a6577fbe6c8065807a3a5d75b456d6805 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 4 Oct 2022 16:06:25 -0300 Subject: [PATCH 4/4] TS --- .../app/ui-message/client/messageBox/messageBoxActions.ts | 2 +- apps/meteor/app/ui/client/lib/fileUpload.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts index e6297e8c734f..e99847f8447e 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts @@ -53,7 +53,7 @@ messageBox.actions.add('Add_files_from', 'Computer', { }; }); - fileUpload(filesToUpload, $('.js-input-message', messageBox).get(0) as HTMLTextAreaElement | undefined, { rid, tmid }); + fileUpload(filesToUpload, $('.js-input-message', messageBox).get(0) as HTMLTextAreaElement, { rid, tmid }); $input.remove(); }); diff --git a/apps/meteor/app/ui/client/lib/fileUpload.ts b/apps/meteor/app/ui/client/lib/fileUpload.ts index 780e149abb9b..84935425da83 100644 --- a/apps/meteor/app/ui/client/lib/fileUpload.ts +++ b/apps/meteor/app/ui/client/lib/fileUpload.ts @@ -158,7 +158,7 @@ export type FileUploadProp = SingleOrArray<{ /* @deprecated */ export const fileUpload = async ( f: FileUploadProp, - input: HTMLInputElement | ArrayLike, + input: HTMLInputElement | ArrayLike | HTMLTextAreaElement, { rid, tmid,