From afe1b2cb2fd4f9bee586beb6f4993e754c63b215 Mon Sep 17 00:00:00 2001 From: Matvei Andrienko Date: Wed, 26 Feb 2025 16:46:02 +0100 Subject: [PATCH 1/4] add explicit default device option to device selectors --- .../DeviceSettings/DeviceSelector.tsx | 87 ++++++++----------- .../useDeviceListWithDefault.tsx | 50 +++++++++++ 2 files changed, 86 insertions(+), 51 deletions(-) create mode 100644 packages/react-sdk/src/components/DeviceSettings/useDeviceListWithDefault.tsx diff --git a/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx b/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx index 50131061be..0458300a5c 100644 --- a/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx +++ b/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx @@ -3,7 +3,7 @@ import { ChangeEventHandler, useCallback } from 'react'; import { DropDownSelect, DropDownSelectOption } from '../DropdownSelect'; import { useMenuContext } from '../Menu'; -import { useI18n } from '@stream-io/video-react-bindings'; +import { useDeviceListWithDefault } from './useDeviceListWithDefault'; type DeviceSelectorOptionProps = { id: string; @@ -60,7 +60,7 @@ const DeviceSelectorList = (props: { }) => { const { devices = [], selectedDeviceId, title, type, onChange } = props; const { close } = useMenuContext(); - const { t } = useI18n(); + const { deviceList } = useDeviceListWithDefault(devices, selectedDeviceId); return (
@@ -69,34 +69,25 @@ const DeviceSelectorList = (props: { {title}
)} - {devices.length === 0 ? ( - - ) : ( - devices.map((device) => { - return ( - { - onChange?.(e.target.value); - close?.(); - }} - name={type} - selected={ - device.deviceId === selectedDeviceId || devices.length === 1 + {deviceList.map((device) => { + return ( + { + const deviceId = e.target.value; + if (deviceId !== 'default') { + onChange?.(deviceId); } - /> - ); - }) - )} + close?.(); + }} + name={type} + selected={device.isSelected} + /> + ); + })} ); }; @@ -109,17 +100,17 @@ const DeviceSelectorDropdown = (props: { icon: string; }) => { const { devices = [], selectedDeviceId, title, onChange, icon } = props; - const { t } = useI18n(); - - const selectedIndex = devices.findIndex( - (d) => d.deviceId === selectedDeviceId, - ); + const { deviceList, selectedDevice, selectedIndex } = + useDeviceListWithDefault(devices, selectedDeviceId); const handleSelect = useCallback( (index: number) => { - onChange?.(devices[index].deviceId); + const deviceId = deviceList[index].deviceId; + if (deviceId !== 'default') { + onChange?.(deviceId); + } }, - [devices, onChange], + [deviceList, onChange], ); return ( @@ -130,23 +121,17 @@ const DeviceSelectorDropdown = (props: { - {devices.length === 0 ? ( - - ) : ( - devices.map((device) => ( - - )) - )} + {deviceList.map((device) => ( + + ))} ); diff --git a/packages/react-sdk/src/components/DeviceSettings/useDeviceListWithDefault.tsx b/packages/react-sdk/src/components/DeviceSettings/useDeviceListWithDefault.tsx new file mode 100644 index 0000000000..6e5c1cbbe7 --- /dev/null +++ b/packages/react-sdk/src/components/DeviceSettings/useDeviceListWithDefault.tsx @@ -0,0 +1,50 @@ +import { useI18n } from '@stream-io/video-react-bindings'; +import { useMemo } from 'react'; + +export interface DeviceListItem { + deviceId: string; + label: string; + isSelected: boolean; +} + +export function useDeviceListWithDefault( + devices: MediaDeviceInfo[], + selectedDeviceId: string | undefined, +): { + deviceList: DeviceListItem[]; + selectedDevice: DeviceListItem; + selectedIndex: number; +} { + const { t } = useI18n(); + + return useMemo(() => { + let selectedDevice: DeviceListItem | null = null; + let selectedIndex: number | null = null; + + const deviceList: DeviceListItem[] = devices.map((d, i) => { + const isSelected = d.deviceId === selectedDeviceId; + const device = { deviceId: d.deviceId, label: d.label, isSelected }; + + if (isSelected) { + selectedDevice = device; + selectedIndex = i; + } + + return device; + }); + + if (selectedDevice === null || selectedIndex === null) { + const defaultDevice = { + deviceId: 'default', + label: t('Default'), + isSelected: true, + }; + + selectedDevice = defaultDevice; + selectedIndex = 0; + deviceList.unshift(defaultDevice); + } + + return { deviceList, selectedDevice, selectedIndex }; + }, [devices, selectedDeviceId, t]); +} From 819fbbe3c18813743c9f0812061e8f4e9cd34c1c Mon Sep 17 00:00:00 2001 From: Matvei Andrienko Date: Wed, 26 Feb 2025 17:22:41 +0100 Subject: [PATCH 2/4] make useDeviceList hook public --- .../src/components/DeviceSettings/DeviceSelector.tsx | 10 ++++++---- packages/react-sdk/src/hooks/index.ts | 1 + .../useDeviceList.tsx} | 9 ++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) rename packages/react-sdk/src/{components/DeviceSettings/useDeviceListWithDefault.tsx => hooks/useDeviceList.tsx} (73%) diff --git a/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx b/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx index 0458300a5c..94a7625015 100644 --- a/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx +++ b/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx @@ -1,9 +1,9 @@ import clsx from 'clsx'; import { ChangeEventHandler, useCallback } from 'react'; +import { useDeviceList } from '../../hooks/useDeviceList'; import { DropDownSelect, DropDownSelectOption } from '../DropdownSelect'; import { useMenuContext } from '../Menu'; -import { useDeviceListWithDefault } from './useDeviceListWithDefault'; type DeviceSelectorOptionProps = { id: string; @@ -60,7 +60,7 @@ const DeviceSelectorList = (props: { }) => { const { devices = [], selectedDeviceId, title, type, onChange } = props; const { close } = useMenuContext(); - const { deviceList } = useDeviceListWithDefault(devices, selectedDeviceId); + const { deviceList } = useDeviceList(devices, selectedDeviceId); return (
@@ -100,8 +100,10 @@ const DeviceSelectorDropdown = (props: { icon: string; }) => { const { devices = [], selectedDeviceId, title, onChange, icon } = props; - const { deviceList, selectedDevice, selectedIndex } = - useDeviceListWithDefault(devices, selectedDeviceId); + const { deviceList, selectedDevice, selectedIndex } = useDeviceList( + devices, + selectedDeviceId, + ); const handleSelect = useCallback( (index: number) => { diff --git a/packages/react-sdk/src/hooks/index.ts b/packages/react-sdk/src/hooks/index.ts index 75750def85..97ac89503b 100644 --- a/packages/react-sdk/src/hooks/index.ts +++ b/packages/react-sdk/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from './useFloatingUIPreset'; export * from './usePersistedDevicePreferences'; export * from './useScrollPosition'; export * from './useRequestPermission'; +export * from './useDeviceList'; diff --git a/packages/react-sdk/src/components/DeviceSettings/useDeviceListWithDefault.tsx b/packages/react-sdk/src/hooks/useDeviceList.tsx similarity index 73% rename from packages/react-sdk/src/components/DeviceSettings/useDeviceListWithDefault.tsx rename to packages/react-sdk/src/hooks/useDeviceList.tsx index 6e5c1cbbe7..4e9ab478ea 100644 --- a/packages/react-sdk/src/components/DeviceSettings/useDeviceListWithDefault.tsx +++ b/packages/react-sdk/src/hooks/useDeviceList.tsx @@ -7,7 +7,14 @@ export interface DeviceListItem { isSelected: boolean; } -export function useDeviceListWithDefault( +/** + * Utility hook that helps render a list of devices or implement a device selector. + * Compared someting like `useCameraState().devices`, it has some handy features: + * 1. Adds the "Default" device to the list if applicable (either the user did not + * select a device, or a previously selected device is no longer available). + * 2. Maps the device list to a format more suitable for rendering. + */ +export function useDeviceList( devices: MediaDeviceInfo[], selectedDeviceId: string | undefined, ): { From b80e0cb04e0bb330c39361d6b67f8c4309264633 Mon Sep 17 00:00:00 2001 From: Matvei Andrienko Date: Wed, 26 Feb 2025 17:28:01 +0100 Subject: [PATCH 3/4] rename selectedDevice to avoid name collision --- .../src/components/DeviceSettings/DeviceSelector.tsx | 4 ++-- packages/react-sdk/src/hooks/useDeviceList.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx b/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx index 94a7625015..7478a816cb 100644 --- a/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx +++ b/packages/react-sdk/src/components/DeviceSettings/DeviceSelector.tsx @@ -100,7 +100,7 @@ const DeviceSelectorDropdown = (props: { icon: string; }) => { const { devices = [], selectedDeviceId, title, onChange, icon } = props; - const { deviceList, selectedDevice, selectedIndex } = useDeviceList( + const { deviceList, selectedDeviceInfo, selectedIndex } = useDeviceList( devices, selectedDeviceId, ); @@ -123,7 +123,7 @@ const DeviceSelectorDropdown = (props: { {deviceList.map((device) => ( diff --git a/packages/react-sdk/src/hooks/useDeviceList.tsx b/packages/react-sdk/src/hooks/useDeviceList.tsx index 4e9ab478ea..78fc797e30 100644 --- a/packages/react-sdk/src/hooks/useDeviceList.tsx +++ b/packages/react-sdk/src/hooks/useDeviceList.tsx @@ -19,13 +19,13 @@ export function useDeviceList( selectedDeviceId: string | undefined, ): { deviceList: DeviceListItem[]; - selectedDevice: DeviceListItem; + selectedDeviceInfo: DeviceListItem; selectedIndex: number; } { const { t } = useI18n(); return useMemo(() => { - let selectedDevice: DeviceListItem | null = null; + let selectedDeviceInfo: DeviceListItem | null = null; let selectedIndex: number | null = null; const deviceList: DeviceListItem[] = devices.map((d, i) => { @@ -33,25 +33,25 @@ export function useDeviceList( const device = { deviceId: d.deviceId, label: d.label, isSelected }; if (isSelected) { - selectedDevice = device; + selectedDeviceInfo = device; selectedIndex = i; } return device; }); - if (selectedDevice === null || selectedIndex === null) { + if (selectedDeviceInfo === null || selectedIndex === null) { const defaultDevice = { deviceId: 'default', label: t('Default'), isSelected: true, }; - selectedDevice = defaultDevice; + selectedDeviceInfo = defaultDevice; selectedIndex = 0; deviceList.unshift(defaultDevice); } - return { deviceList, selectedDevice, selectedIndex }; + return { deviceList, selectedDeviceInfo, selectedIndex }; }, [devices, selectedDeviceId, t]); } From 8bb3b22984cf5f6fc1012ea365bc3054e247f458 Mon Sep 17 00:00:00 2001 From: Matvei Andrienko Date: Wed, 26 Feb 2025 17:37:56 +0100 Subject: [PATCH 4/4] fix typo --- packages/react-sdk/src/hooks/useDeviceList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-sdk/src/hooks/useDeviceList.tsx b/packages/react-sdk/src/hooks/useDeviceList.tsx index 78fc797e30..611682e9f4 100644 --- a/packages/react-sdk/src/hooks/useDeviceList.tsx +++ b/packages/react-sdk/src/hooks/useDeviceList.tsx @@ -9,7 +9,7 @@ export interface DeviceListItem { /** * Utility hook that helps render a list of devices or implement a device selector. - * Compared someting like `useCameraState().devices`, it has some handy features: + * Compared to someting like `useCameraState().devices`, it has some handy features: * 1. Adds the "Default" device to the list if applicable (either the user did not * select a device, or a previously selected device is no longer available). * 2. Maps the device list to a format more suitable for rendering.