diff --git a/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx b/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx index e695691f19d..8ab12e850b3 100644 --- a/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx +++ b/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx @@ -1,5 +1,6 @@ -import { DeviceContext, Device, IExperimentalHTMLAudioElement } from '@rocket.chat/ui-contexts'; -import React, { ReactElement, ReactNode, useEffect, useState } from 'react'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { DeviceContext, Device, IExperimentalHTMLAudioElement, DeviceContextValue } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, ReactNode, useEffect, useState, useMemo } from 'react'; import { isSetSinkIdAvailable } from './lib/isSetSinkIdAvailable'; @@ -8,6 +9,7 @@ type DeviceProviderProps = { }; export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement => { + const [enabled] = useState(typeof isSecureContext && isSecureContext); const [availableAudioOutputDevices, setAvailableAudioOutputDevices] = useState([]); const [availableAudioInputDevices, setAvailableAudioInputDevices] = useState([]); const [selectedAudioOutputDevice, setSelectedAudioOutputDevice] = useState({ @@ -21,23 +23,32 @@ export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement type: 'audio', }); - const setAudioOutputDevice = ({ - outputDevice, - HTMLAudioElement, - }: { - outputDevice: Device; - HTMLAudioElement: IExperimentalHTMLAudioElement; - }): void => { - if (!isSetSinkIdAvailable()) { - throw new Error('setSinkId is not available in this browser'); + const setAudioInputDevice = (device: Device): void => { + if (!isSecureContext) { + throw new Error('Device Changes are not available on insecure contexts'); } - setSelectedAudioOutputDevice(outputDevice); - HTMLAudioElement.setSinkId(outputDevice.id); + setSelectedAudioInputDevice(device); }; + const setAudioOutputDevice = useMutableCallback( + ({ outputDevice, HTMLAudioElement }: { outputDevice: Device; HTMLAudioElement: IExperimentalHTMLAudioElement }): void => { + if (!isSetSinkIdAvailable()) { + throw new Error('setSinkId is not available in this browser'); + } + if (!enabled) { + throw new Error('Device Changes are not available on insecure contexts'); + } + setSelectedAudioOutputDevice(outputDevice); + HTMLAudioElement.setSinkId(outputDevice.id); + }, + ); + useEffect(() => { + if (!enabled) { + return; + } const setMediaDevices = (): void => { - navigator.mediaDevices.enumerateDevices().then((devices) => { + navigator.mediaDevices?.enumerateDevices().then((devices) => { const audioInput: Device[] = []; const audioOutput: Device[] = []; devices.forEach((device) => { @@ -57,21 +68,37 @@ export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement }); }; - navigator.mediaDevices.addEventListener('devicechange', setMediaDevices); + navigator.mediaDevices?.addEventListener('devicechange', setMediaDevices); setMediaDevices(); return (): void => { - navigator.mediaDevices.removeEventListener('devicechange', setMediaDevices); + navigator.mediaDevices?.removeEventListener('devicechange', setMediaDevices); }; - }, []); + }, [enabled]); - const contextValue = { - availableAudioOutputDevices, + const contextValue = useMemo((): DeviceContextValue => { + if (!enabled) { + return { + enabled, + }; + } + + return { + enabled, + availableAudioOutputDevices, + availableAudioInputDevices, + selectedAudioOutputDevice, + selectedAudioInputDevice, + setAudioOutputDevice, + setAudioInputDevice, + }; + }, [ availableAudioInputDevices, - selectedAudioOutputDevice, + availableAudioOutputDevices, + enabled, selectedAudioInputDevice, + selectedAudioOutputDevice, setAudioOutputDevice, - setAudioInputDevice: setSelectedAudioInputDevice, - }; + ]); return {children}; }; diff --git a/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx b/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx index d29f238e598..74b5ae009bb 100644 --- a/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx +++ b/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx @@ -1,5 +1,12 @@ import { Modal, Field, Select, ButtonGroup, Button, SelectOption, Box } from '@rocket.chat/fuselage'; -import { useTranslation, useAvailableDevices, useToastMessageDispatch, useSetModal, useSelectedDevices } from '@rocket.chat/ui-contexts'; +import { + useTranslation, + useAvailableDevices, + useToastMessageDispatch, + useSetModal, + useSelectedDevices, + useIsDeviceManagementEnabled, +} from '@rocket.chat/ui-contexts'; import React, { ReactElement, useState } from 'react'; import { useForm, Controller, SubmitHandler } from 'react-hook-form'; @@ -14,6 +21,7 @@ type FieldValues = { const DeviceSettingsModal = (): ReactElement => { const setModal = useSetModal(); const onCancel = (): void => setModal(); + const isDeviceManagementEnabled = useIsDeviceManagementEnabled(); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const selectedAudioDevices = useSelectedDevices(); @@ -60,6 +68,11 @@ const DeviceSettingsModal = (): ReactElement => { )} + {!isDeviceManagementEnabled && ( + + {t('Device_Changes_Not_Available_Insecure_Context')} + + )} {t('Microphone')} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index fd245398d31..81177d35e74 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1486,6 +1486,7 @@ "Desktop_Notifications_Not_Enabled": "Desktop Notifications are Not Enabled", "Details": "Details", "Device_Changes_Not_Available": "Device changes not available in this browser. For guaranteed availability, please use Rocket.Chat's official desktop app.", + "Device_Changes_Not_Available_Insecure_Context": "Device changes are only available on secure contexts (e.g. https://)", "Device_Management": "Device management", "Device_ID": "Device ID", "Device_Info": "Device Info", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index c5298d15632..e05394a481e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -1438,6 +1438,7 @@ "Desktop_Notifications_Not_Enabled": "Notificações da área de trabalho estão desabilitadas", "Details": "Detalhes", "Device_Changes_Not_Available": "Mudanças de dispositivo não estão disponíveis neste navegador, para disponíbilidade garantida, use o aplicativo desktop oficial do Rocket.Chat.", + "Device_Changes_Not_Available_Insecure_Context": "Mudanças de dispositivo somente estão disponíveis em contextos seguros. (https://)", "Different_Style_For_User_Mentions": "Estilo diferente para as menções do usuário", "Direct_Message": "Mensagem direta", "Direct_message_creation_description": "Você está prestes a criar uma conversa com vários usuários. Adicione os usuários com quem gostaria de conversar, todos no mesmo local, utilizando mensagens diretas.", diff --git a/packages/ui-contexts/src/DeviceContext.ts b/packages/ui-contexts/src/DeviceContext.ts index 545bb435337..483f03707f9 100644 --- a/packages/ui-contexts/src/DeviceContext.ts +++ b/packages/ui-contexts/src/DeviceContext.ts @@ -10,7 +10,8 @@ export interface IExperimentalHTMLAudioElement extends HTMLAudioElement { setSinkId: (sinkId: string) => void; } -type DeviceContextValue = { +type EnabledDeviceContextValue = { + enabled: true; availableAudioOutputDevices: Device[]; availableAudioInputDevices: Device[]; // availableVideoInputDevices: Device[] @@ -22,22 +23,15 @@ type DeviceContextValue = { // setVideoInputDevice: (device: Device) => void; }; +type DisabledDeviceContextValue = { + enabled: false; +}; + +export type DeviceContextValue = EnabledDeviceContextValue | DisabledDeviceContextValue; + +export const isDeviceContextEnabled = (context: DeviceContextValue): context is EnabledDeviceContextValue => + (context as EnabledDeviceContextValue).enabled; + export const DeviceContext = createContext({ - availableAudioOutputDevices: [], - availableAudioInputDevices: [], - // availableVideoInputDevices: [], - selectedAudioOutputDevice: { - id: 'default', - label: '', - type: 'audio', - }, - selectedAudioInputDevice: { - id: 'default', - label: '', - type: 'audio', - }, - // selectedVideoInputDevice: undefined, - setAudioOutputDevice: () => undefined, - setAudioInputDevice: () => undefined, - // setVideoInputDevice: () => undefined, + enabled: false, }); diff --git a/packages/ui-contexts/src/hooks/useAvailableDevices.ts b/packages/ui-contexts/src/hooks/useAvailableDevices.ts index 5824c02e637..59f43db533f 100644 --- a/packages/ui-contexts/src/hooks/useAvailableDevices.ts +++ b/packages/ui-contexts/src/hooks/useAvailableDevices.ts @@ -1,13 +1,24 @@ import { useContext } from 'react'; -import { DeviceContext, Device } from '../DeviceContext'; +import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext'; type AvailableDevices = { audioInput?: Device[]; audioOutput?: Device[]; }; -export const useAvailableDevices = (): AvailableDevices => ({ - audioInput: useContext(DeviceContext).availableAudioInputDevices, - audioOutput: useContext(DeviceContext).availableAudioOutputDevices, -}); +export const useAvailableDevices = (): AvailableDevices | null => { + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return null; + } + + return { + audioInput: context.availableAudioInputDevices, + audioOutput: context.availableAudioOutputDevices, + }; +}; diff --git a/packages/ui-contexts/src/hooks/useDeviceConstraints.ts b/packages/ui-contexts/src/hooks/useDeviceConstraints.ts index e21e318204b..c7320159e42 100644 --- a/packages/ui-contexts/src/hooks/useDeviceConstraints.ts +++ b/packages/ui-contexts/src/hooks/useDeviceConstraints.ts @@ -1,8 +1,17 @@ import { useContext } from 'react'; -import { DeviceContext } from '../DeviceContext'; +import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext'; -export const useDeviceConstraints = (): MediaStreamConstraints => { - const selectedAudioInputDeviceId = useContext(DeviceContext).selectedAudioInputDevice?.id; +export const useDeviceConstraints = (): MediaStreamConstraints | null => { + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return null; + } + + const selectedAudioInputDeviceId = context.selectedAudioInputDevice?.id; return { audio: selectedAudioInputDeviceId === 'default' ? true : { deviceId: { exact: selectedAudioInputDeviceId } } }; }; diff --git a/packages/ui-contexts/src/hooks/useIsDeviceManagementEnabled.ts b/packages/ui-contexts/src/hooks/useIsDeviceManagementEnabled.ts new file mode 100644 index 00000000000..a453b516954 --- /dev/null +++ b/packages/ui-contexts/src/hooks/useIsDeviceManagementEnabled.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { DeviceContext } from '../DeviceContext'; + +export const useIsDeviceManagementEnabled = (): boolean => useContext(DeviceContext).enabled; diff --git a/packages/ui-contexts/src/hooks/useSelectedDevices.ts b/packages/ui-contexts/src/hooks/useSelectedDevices.ts index ce26b658b1c..2a682d4ce8d 100644 --- a/packages/ui-contexts/src/hooks/useSelectedDevices.ts +++ b/packages/ui-contexts/src/hooks/useSelectedDevices.ts @@ -1,13 +1,24 @@ import { useContext } from 'react'; -import { DeviceContext, Device } from '../DeviceContext'; +import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext'; type SelectedDevices = { audioInput?: Device; audioOutput?: Device; }; -export const useSelectedDevices = (): SelectedDevices => ({ - audioInput: useContext(DeviceContext).selectedAudioInputDevice, - audioOutput: useContext(DeviceContext).selectedAudioOutputDevice, -}); +export const useSelectedDevices = (): SelectedDevices | null => { + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return null; + } + + return { + audioInput: context.selectedAudioInputDevice, + audioOutput: context.selectedAudioOutputDevice, + }; +}; diff --git a/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts b/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts index 801cf014802..619c1899054 100644 --- a/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts +++ b/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts @@ -1,9 +1,17 @@ import { useContext } from 'react'; -import { DeviceContext, Device } from '../DeviceContext'; +import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext'; type setInputMediaDevice = (inputDevice: Device) => void; export const useSetInputMediaDevice = (): setInputMediaDevice => { - return useContext(DeviceContext).setAudioInputDevice; + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return () => undefined; + } + return context.setAudioInputDevice; }; diff --git a/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts b/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts index 531ad1607a0..a7832b52bc5 100644 --- a/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts +++ b/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import { DeviceContext, Device, IExperimentalHTMLAudioElement } from '../DeviceContext'; +import { DeviceContext, Device, IExperimentalHTMLAudioElement, isDeviceContextEnabled } from '../DeviceContext'; // This allows different places to set the output device by providing a HTMLAudioElement @@ -13,5 +13,14 @@ type setOutputMediaDevice = ({ }) => void; export const useSetOutputMediaDevice = (): setOutputMediaDevice => { - return useContext(DeviceContext).setAudioOutputDevice; + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return () => undefined; + } + + return context.setAudioOutputDevice; }; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index bb57ddd38bb..0e456969d1c 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -13,7 +13,7 @@ export { ToastMessagesContext, ToastMessagesContextValue } from './ToastMessages export { TooltipContext, TooltipContextValue } from './TooltipContext'; export { TranslationContext, TranslationContextValue } from './TranslationContext'; export { UserContext, UserContextValue } from './UserContext'; -export { DeviceContext, Device, IExperimentalHTMLAudioElement } from './DeviceContext'; +export { DeviceContext, Device, IExperimentalHTMLAudioElement, DeviceContextValue } from './DeviceContext'; export { useAbsoluteUrl } from './hooks/useAbsoluteUrl'; export { useAllPermissions } from './hooks/useAllPermissions'; @@ -76,6 +76,7 @@ export { useUserSubscriptions } from './hooks/useUserSubscriptions'; export { useSelectedDevices } from './hooks/useSelectedDevices'; export { useDeviceConstraints } from './hooks/useDeviceConstraints'; export { useAvailableDevices } from './hooks/useAvailableDevices'; +export { useIsDeviceManagementEnabled } from './hooks/useIsDeviceManagementEnabled'; export { useSetOutputMediaDevice } from './hooks/useSetOutputMediaDevice'; export { useSetInputMediaDevice } from './hooks/useSetInputMediaDevice';