Regression: Denied Microphone permission disables composer (#28133)

Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com>
pull/28048/head^2
Yash Rajpal 3 years ago committed by GitHub
parent 02b69c59a0
commit fe1a36e889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts
  2. 3
      apps/meteor/client/lib/chats/ChatAPI.ts
  3. 68
      apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx
  4. 8
      apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx
  5. 12
      apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx
  6. 97
      apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx
  7. 3
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json

@ -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,
};
};

@ -53,6 +53,9 @@ export type ComposerAPI = {
setRecordingVideo(recording: boolean): void;
readonly recordingVideo: Subscribable<boolean>;
setIsMicrophoneDenied(isMicrophoneDenied: boolean): void;
readonly isMicrophoneDenied: Subscribable<boolean>;
readonly formatters: Subscribable<FormattingButton[]>;
};

@ -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<AllHTMLAttributes<HTMLDivElement>, '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<ReturnType<typeof setInterval> | null>(null);
const [recordingRoomId, setRecordingRoomId] = useState<IRoom['_id'] | null>(null);
@ -36,42 +35,13 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps):
setTime('00:00');
const blob = await new Promise<Blob>((resolve) => audioRecorder.stop(resolve));
chat?.action.stop('recording');
chat?.composer?.setRecordingMode(false);
return blob;
});
const handleMount = useMutableCallback(async (): Promise<void> => {
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<Blob>((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;
}

@ -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 && <VideoMessageRecorder reference={messageComposerRef} rid={rid} tmid={tmid} />}
<MessageComposer ref={messageComposerRef} variant={isEditing ? 'editing' : undefined}>
{isRecordingAudio && <AudioMessageRecorder rid={rid} tmid={tmid} disabled={!canSend || typing} />}
{isRecordingAudio && <AudioMessageRecorder rid={rid} isMicrophoneDenied={isMicrophoneDenied} />}
<MessageComposerInput
ref={mergedRefs as unknown as Ref<HTMLInputElement>}
aria-label={t('Message')}
@ -421,6 +426,7 @@ const MessageBox = ({
canJoin={canJoin}
rid={rid}
tmid={tmid}
isMicrophoneDenied={isMicrophoneDenied}
/>
</MessageComposerToolbarActions>
<MessageComposerToolbarSubmit>

@ -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 = [
<VideoMessageAction key='video' collapsed={variant === 'small'} disabled={(!canJoin && !canSend) || typing || isRecording} />,
<AudioMessageAction key='audio' disabled={(!canJoin && !canSend) || typing || isRecording} />,
<AudioMessageAction
key='audio'
disabled={(!canJoin && !canSend) || typing || isRecording || isMicrophoneDenied}
isMicrophoneDenied={isMicrophoneDenied}
/>,
<FileUploadAction key='file' collapsed={variant === 'small'} disabled={!canSend || isRecording} />,
];

@ -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<AllHTMLAttributes<HTMLButtonElement>, '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<void> => {
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 (
<MessageComposerAction
title={t('Audio_message')}
title={getTranslationKey}
icon='mic'
disabled={disabled || !isAllowed}
className='rc-message-box__icon rc-message-box__audio-message-mic'
data-qa-id='audio-record'
onClick={handleRecordButtonClick}

@ -2178,6 +2178,7 @@
"File_Type": "File Type",
"File_type_is_not_accepted": "File type is not accepted.",
"File_uploaded": "File uploaded",
"File_Upload_Disabled": "File upload disabled",
"File_uploaded_successfully": "File uploaded successfully",
"File_URL": "File URL",
"FileType": "File Type",
@ -3255,6 +3256,7 @@
"Message_Audio_bitRate": "Audio Message Bit Rate",
"Message_AudioRecorderEnabled": "Audio Recorder Enabled",
"Message_AudioRecorderEnabled_Description": "Requires 'audio/mp3' files to be an accepted media type within 'File Upload' settings.",
"Message_Audio_Recording_Disabled": "Message audio recording disabled",
"Message_auditing": "Message auditing",
"Message_auditing_log": "Message auditing log",
"Message_BadWordsFilterList": "Add Bad Words to the Blacklist",
@ -3377,6 +3379,7 @@
"Method": "Method",
"Mic_on": "Mic On",
"Microphone": "Microphone",
"Microphone_access_not_allowed": "Microphone access was not allowed, please check your browser settings.",
"Mic_off": "Mic Off",
"Min_length_is": "Min length is %s",
"Minimum": "Minimum",

Loading…
Cancel
Save