diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index f648566203e..d92bbc125a9 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -21,7 +21,14 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) input.dispatchEvent(event); }; - const emitter = new Emitter<{ quotedMessagesUpdate: void; editing: void; recording: void; recordingVideo: void; formatting: void }>(); + const emitter = new Emitter<{ + quotedMessagesUpdate: void; + editing: void; + recording: void; + recordingVideo: void; + formatting: void; + mircophoneDenied: void; + }>(); let _quotedMessages: IMessage[] = []; @@ -167,6 +174,21 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) ]; })(); + const [isMicrophoneDenied, setIsMicrophoneDenied] = (() => { + let isMicrophoneDenied = false; + + return [ + { + get: () => isMicrophoneDenied, + subscribe: (callback: () => void) => emitter.on('mircophoneDenied', callback), + }, + (value: boolean) => { + isMicrophoneDenied = value; + emitter.emit('mircophoneDenied'); + }, + ]; + })(); + const setEditingMode = (editing: boolean): void => { setEditing(editing); }; @@ -317,5 +339,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) dismissAllQuotedMessages, quotedMessages, formatters, + isMicrophoneDenied, + setIsMicrophoneDenied, }; }; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index a96b81c44ff..a7891cc3dcd 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -53,6 +53,9 @@ export type ComposerAPI = { setRecordingVideo(recording: boolean): void; readonly recordingVideo: Subscribable; + setIsMicrophoneDenied(isMicrophoneDenied: boolean): void; + readonly isMicrophoneDenied: Subscribable; + readonly formatters: Subscribable; }; diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx index f6448220296..bfe2b0a8f2f 100644 --- a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx @@ -1,9 +1,9 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { Box, Throbber } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement, AllHTMLAttributes } from 'react'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useState } from 'react'; import { AudioRecorder } from '../../../../app/ui/client'; @@ -14,16 +14,15 @@ const audioRecorder = new AudioRecorder(); type AudioMessageRecorderProps = { rid: IRoom['_id']; - tmid?: IMessage['_id']; chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React -} & Omit, 'is'>; + isMicrophoneDenied?: boolean; +}; -const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): ReactElement | null => { +const AudioMessageRecorder = ({ rid, chatContext, isMicrophoneDenied }: AudioMessageRecorderProps): ReactElement | null => { const t = useTranslation(); const [state, setState] = useState<'loading' | 'recording'>('recording'); const [time, setTime] = useState('00:00'); - const [isMicrophoneDenied, setIsMicrophoneDenied] = useState(false); const [recordingInterval, setRecordingInterval] = useState | null>(null); const [recordingRoomId, setRecordingRoomId] = useState(null); @@ -36,42 +35,13 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): setTime('00:00'); - const blob = await new Promise((resolve) => audioRecorder.stop(resolve)); - chat?.action.stop('recording'); chat?.composer?.setRecordingMode(false); - 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; - } + const blob = await new Promise((resolve) => audioRecorder.stop(resolve)); - try { - if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { - setIsMicrophoneDenied(true); - return; - } - } catch (error) { - console.warn(error); - } + return blob; }); const handleUnmount = useMutableCallback(async () => { @@ -101,7 +71,6 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): setRecordingRoomId(rid); } catch (error) { console.log(error); - setIsMicrophoneDenied(true); chat?.composer?.setRecordingMode(false); } }); @@ -124,29 +93,12 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): }); useEffect(() => { - handleMount(); handleRecord(); return () => { handleUnmount(); }; - }, [handleMount, handleUnmount, handleRecord]); - - 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], - ); + }, [handleUnmount, handleRecord]); const stateClass = useMemo(() => { if (recordingRoomId && recordingRoomId !== rid) { @@ -156,7 +108,7 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): return state && `rc-message-box__audio-message--${state}`; }, [recordingRoomId, rid, state]); - if (!isAllowed) { + if (isMicrophoneDenied) { return null; } diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx index 8bd92d8dd9f..b7c2f238e6c 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx @@ -261,6 +261,11 @@ const MessageBox = ({ subscribe: chat.composer?.recording.subscribe ?? emptySubscribe, }); + const isMicrophoneDenied = useSubscription({ + getCurrentValue: chat.composer?.isMicrophoneDenied.get ?? getEmptyFalse, + subscribe: chat.composer?.isMicrophoneDenied.subscribe ?? emptySubscribe, + }); + const isRecordingVideo = useSubscription({ getCurrentValue: chat.composer?.recordingVideo.get ?? getEmptyFalse, subscribe: chat.composer?.recordingVideo.subscribe ?? emptySubscribe, @@ -381,7 +386,7 @@ const MessageBox = ({ {isRecordingVideo && } - {isRecordingAudio && } + {isRecordingAudio && } } aria-label={t('Message')} @@ -421,6 +426,7 @@ const MessageBox = ({ canJoin={canJoin} rid={rid} tmid={tmid} + isMicrophoneDenied={isMicrophoneDenied} /> diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index c4a0888b33f..9ea65fd8c44 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -1,4 +1,4 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage } from '@rocket.chat/core-typings'; import React, { memo } from 'react'; import ActionsToolbarDropdown from './ActionsToolbarDropdown'; @@ -13,7 +13,8 @@ type MessageBoxActionsToolbarProps = { canSend: boolean; canJoin: boolean; rid: IRoom['_id']; - tmid?: string; + tmid?: IMessage['_id']; + isMicrophoneDenied?: boolean; }; const MessageBoxActionsToolbar = ({ @@ -24,10 +25,15 @@ const MessageBoxActionsToolbar = ({ rid, tmid, canJoin, + isMicrophoneDenied, }: MessageBoxActionsToolbarProps) => { const actions = [ , - , + , , ]; diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx index ec97e93b013..a7e68d432cc 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx @@ -1,25 +1,114 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import type { AllHTMLAttributes } from 'react'; -import React from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { AudioRecorder } from '../../../../../../../../../app/ui/client'; import type { ChatAPI } from '../../../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../../../contexts/ChatContext'; +const audioRecorder = new AudioRecorder(); + type AudioMessageActionProps = { chatContext?: ChatAPI; + isMicrophoneDenied?: boolean; } & Omit, 'is'>; -const AudioMessageAction = ({ chatContext, ...props }: AudioMessageActionProps) => { +const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...props }: AudioMessageActionProps) => { const t = useTranslation(); const chat = useChat() ?? chatContext; + const stopRecording = useMutableCallback(() => { + chat?.action.stop('recording'); + + chat?.composer?.setRecordingMode(false); + }); + + const setMicrophoneDenied = useMutableCallback((isDenied) => { + if (isDenied) { + stopRecording(); + } + + chat?.composer?.setIsMicrophoneDenied(isDenied); + }); + const handleRecordButtonClick = () => chat?.composer?.setRecordingMode(true); + const handleMount = useMutableCallback(async (): Promise => { + if (navigator.permissions) { + try { + const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName }); + setMicrophoneDenied(permissionStatus.state === 'denied'); + permissionStatus.onchange = (): void => { + setMicrophoneDenied(permissionStatus.state === 'denied'); + }; + return; + } catch (error) { + console.warn(error); + } + } + + if (!navigator.mediaDevices?.enumerateDevices) { + setMicrophoneDenied(true); + return; + } + + try { + if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { + setMicrophoneDenied(true); + return; + } + } catch (error) { + console.warn(error); + } + }); + + useEffect(() => { + handleMount(); + }, [handleMount]); + + 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?.match(/audio\/mp3|audio\/\*/i) && + (!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/audio\/mp3|audio\/\*/i)), + [fileUploadMediaTypeBlackList, fileUploadMediaTypeWhiteList, isAudioRecorderEnabled, isFileUploadEnabled, isMicrophoneDenied], + ); + + const getTranslationKey = useMemo(() => { + if (isMicrophoneDenied) { + return t('Microphone_access_not_allowed'); + } + + if (!isFileUploadEnabled) { + return t('File_Upload_Disabled'); + } + + if (!isAudioRecorderEnabled) { + return t('Message_Audio_Recording_Disabled'); + } + + if (!isAllowed) { + return t('error-not-allowed'); + } + + return t('Audio_message'); + }, [isMicrophoneDenied, isFileUploadEnabled, isAudioRecorderEnabled, isAllowed, t]); + return (