From 22ded30b6193863fba8711598c66a2e57dca1869 Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Mon, 13 Feb 2023 16:01:08 +0200 Subject: [PATCH] feat(audio-menu) Redesign audio picker menu (#12899) Convert some files to TS Remove unnecessary files Implement redesign Add noise suppression to picker menu Fix Popover placement on browser resize --- css/_audio-preview.scss | 131 ++-------- css/_meter.scss | 8 +- react/features/base/icons/svg/meter.svg | 18 +- react/features/base/settings/reducer.ts | 3 +- .../base/ui/components/web/ContextMenu.tsx | 21 +- .../ui/components/web/ContextMenuItem.tsx | 27 +- ...ngsContent.js => AudioSettingsContent.tsx} | 243 ++++++++++-------- .../web/audio/AudioSettingsEntry.js | 64 ----- .../web/audio/AudioSettingsHeader.js | 48 ---- ...ettingsPopup.js => AudioSettingsPopup.tsx} | 37 ++- .../web/audio/{Meter.js => Meter.tsx} | 17 +- ...MicrophoneEntry.js => MicrophoneEntry.tsx} | 95 ++++--- .../components/web/audio/SpeakerEntry.js | 163 ------------ .../components/web/audio/SpeakerEntry.tsx | 140 ++++++++++ .../components/web/audio/TestButton.js | 34 --- react/features/settings/functions.web.ts | 2 +- .../toolbox/components/web/DialogPortal.ts | 6 +- 17 files changed, 438 insertions(+), 619 deletions(-) rename react/features/settings/components/web/audio/{AudioSettingsContent.js => AudioSettingsContent.tsx} (55%) delete mode 100644 react/features/settings/components/web/audio/AudioSettingsEntry.js delete mode 100644 react/features/settings/components/web/audio/AudioSettingsHeader.js rename react/features/settings/components/web/audio/{AudioSettingsPopup.js => AudioSettingsPopup.tsx} (75%) rename react/features/settings/components/web/audio/{Meter.js => Meter.tsx} (72%) rename react/features/settings/components/web/audio/{MicrophoneEntry.js => MicrophoneEntry.tsx} (73%) delete mode 100644 react/features/settings/components/web/audio/SpeakerEntry.js create mode 100644 react/features/settings/components/web/audio/SpeakerEntry.tsx delete mode 100644 react/features/settings/components/web/audio/TestButton.js diff --git a/css/_audio-preview.scss b/css/_audio-preview.scss index 221fe7479..a7caabd27 100644 --- a/css/_audio-preview.scss +++ b/css/_audio-preview.scss @@ -2,13 +2,13 @@ display: inline-block; &-content { - background: $menuBG; - border-radius: 3px; - font-size: 14px; - line-height: 24px; + position: relative; + right: auto; + margin-bottom: 8px; max-height: 456px; overflow: auto; width: 300px; + &-ul { margin:0; padding:0; @@ -16,90 +16,37 @@ } } - &-header { - color: #fff; - align-items: center; - display: flex; - margin-top: 8px; - padding: 8px 16px; - - &-icon { - display: inline-block; - - svg { - fill: #fff; - } - } - - &--bordered { - border-bottom: 1px solid #4C4D50; - } - - &-text { - margin-left: 12px; - } + &-header:hover { + background-color: initial; + cursor: initial; } - &-entry { - align-items: center; - color: #fff; - cursor: pointer; - display: flex; - padding: 8px 0; - margin-left: 48px; + &-entry-text { + display: inline-block; + text-overflow: ellipsis; + max-width: 213px; + overflow: hidden; + white-space: nowrap; - &--selected { - background: #131519; - cursor: initial; - margin-left: 0; - padding-left: 18px; - } - - &-text { - color: #fff; - display: inline-block; - line-height: 24px; - text-overflow: ellipsis; - max-width: 213px; - overflow: hidden; - white-space: nowrap; + &.left-margin { + margin-left: 36px; } } &-speaker { position: relative; - &-ul { - margin:0; - padding:0; - list-style-type: none; - } - &:hover, &:focus-within, &:focus { - .audio-preview-entry { - background: #36383C; - margin-left: 0; - padding-left: 48px; - - &--selected { - padding-left: 18px; - background: $newToolbarBackgroundColor; - } - } - .audio-preview-test-button { display: inline-block; } .audio-preview-entry-text { max-width: 178px; + margin-right: 0; } } - &:last-child { - padding-bottom: 8px; - } - .audio-preview-entry-text { max-width: 238px; } @@ -108,19 +55,6 @@ &-microphone { position: relative; - &:hover { - .audio-preview-entry { - background: #36383C; - margin-left: 0; - padding-left: 48px; - - &--selected { - background: $newToolbarBackgroundColor; - padding-left: 18px; - } - } - } - &--nometer { .audio-preview-entry-text { max-width: 238px; @@ -140,42 +74,21 @@ display: inline-block; width: 14px; - & svg { - fill: #1C2025; - } - - &--check { - background: #31B76A; - margin-right: 16px; - } - &--exclamation { margin-left: 6px; + & svg { fill: #E54B4B; } } } - &-hr { - border-top: 1px solid #4C4D50; - border-bottom: 0; - } - &-test-button { display: none; - background: #FFF; - border: 1px solid #D1DBE8; - border-radius: 3px; - color: #1C2025; - cursor: pointer; - font-weight: 600; - font-size: 0.8rem; - line-height: 24px; - padding: 2px 8px; + padding: 4px 10px; position: absolute; right: 16px; - top: 5px; + top: 6px; } &-meter-mic { @@ -184,9 +97,7 @@ top: 14px; } - // Override @atlaskit/InlineDialog container which is made with styled components - & > div:nth-child(2) { - outline: none; - padding: 0; + &-checkbox-container { + padding: 10px 16px; } } diff --git a/css/_meter.scss b/css/_meter.scss index 98f779815..57b4281d5 100644 --- a/css/_meter.scss +++ b/css/_meter.scss @@ -3,28 +3,28 @@ display: inline-block; & > svg { - fill: #4E5E6C; + fill: #525252; width: 38px; } } &.metr--disabled { & > svg { - fill: #4E5E6C; + fill: #525252; } } } .metr-l-0 { rect:first-child { - fill: #31B76A; + fill: #1EC26A; } } @for $i from 1 through 7 { .metr-l-#{$i} { rect:nth-child(-n+#{$i+1}) { - fill: #31B76A; + fill: #1EC26A; } } } diff --git a/react/features/base/icons/svg/meter.svg b/react/features/base/icons/svg/meter.svg index 59c349cc8..acaa508fb 100644 --- a/react/features/base/icons/svg/meter.svg +++ b/react/features/base/icons/svg/meter.svg @@ -1,10 +1,10 @@ - - - - - - - - - + + + + + + + + + diff --git a/react/features/base/settings/reducer.ts b/react/features/base/settings/reducer.ts index 6514208fe..cb3f1f5b5 100644 --- a/react/features/base/settings/reducer.ts +++ b/react/features/base/settings/reducer.ts @@ -52,7 +52,7 @@ const DEFAULT_STATE: ISettingsState = { }; export interface ISettingsState { - audioOutputDeviceId?: string | boolean; + audioOutputDeviceId?: string; audioSettingsVisible?: boolean; avatarURL?: string; cameraDeviceId?: string | boolean; @@ -108,6 +108,7 @@ Object.keys(DEFAULT_STATE).forEach(key => { // we want to filter these props, to not be stored as they represent // what is currently opened/used as devices +// @ts-ignore filterSubtree.audioOutputDeviceId = false; filterSubtree.cameraDeviceId = false; filterSubtree.micDeviceId = false; diff --git a/react/features/base/ui/components/web/ContextMenu.tsx b/react/features/base/ui/components/web/ContextMenu.tsx index 44f07f692..1e4fd6e1c 100644 --- a/react/features/base/ui/components/web/ContextMenu.tsx +++ b/react/features/base/ui/components/web/ContextMenu.tsx @@ -34,6 +34,8 @@ const getComputedOuterHeight = (element: HTMLElement) => { interface IProps { + [key: `aria-${string}`]: string; + /** * Accessibility label for menu container. */ @@ -59,6 +61,11 @@ interface IProps { */ hidden?: boolean; + /** + * Optional id. + */ + id?: string; + /** * Whether or not the menu is already in a drawer. */ @@ -98,6 +105,11 @@ interface IProps { * Callback for the mouse leaving the component. */ onMouseLeave?: (e?: React.MouseEvent) => void; + + /** + * Tab index for the menu. + */ + tabIndex?: number; } const MAX_HEIGHT = 400; @@ -146,6 +158,7 @@ const ContextMenu = ({ className, entity, hidden, + id, inDrawer, isDrawerOpen, offsetTarget, @@ -153,7 +166,8 @@ const ContextMenu = ({ onKeyDown, onDrawerClose, onMouseEnter, - onMouseLeave + onMouseLeave, + tabIndex }: IProps) => { const [ isHidden, setIsHidden ] = useState(true); const containerRef = useRef(null); @@ -217,11 +231,14 @@ const ContextMenu = ({ isHidden && styles.contextMenuHidden, className ) } + id = { id } onClick = { onClick } onKeyDown = { onKeyDown } onMouseEnter = { onMouseEnter } onMouseLeave = { onMouseLeave } - ref = { containerRef }> + ref = { containerRef } + role = 'menu' + tabIndex = { tabIndex }> {children} ; }; diff --git a/react/features/base/ui/components/web/ContextMenuItem.tsx b/react/features/base/ui/components/web/ContextMenuItem.tsx index fd78ab883..55ff8f93a 100644 --- a/react/features/base/ui/components/web/ContextMenuItem.tsx +++ b/react/features/base/ui/components/web/ContextMenuItem.tsx @@ -13,6 +13,11 @@ export interface IProps { */ accessibilityLabel: string; + /** + * Component children. + */ + children?: ReactNode; + /** * CSS class name used for custom styles. */ @@ -54,6 +59,11 @@ export interface IProps { */ onKeyPress?: (e?: React.KeyboardEvent) => void; + /** + * Whether the item is marked as selected. + */ + selected?: boolean; + /** * TestId of the element, if any. */ @@ -62,7 +72,7 @@ export interface IProps { /** * Action text. */ - text: string; + text?: string; /** * Class name for the text. @@ -97,6 +107,12 @@ const useStyles = makeStyles()(theme => { } }, + selected: { + borderLeft: `3px solid ${theme.palette.action01Hover}`, + paddingLeft: '13px', + backgroundColor: theme.palette.ui02 + }, + contextMenuItemDisabled: { pointerEvents: 'none' }, @@ -124,6 +140,7 @@ const useStyles = makeStyles()(theme => { const ContextMenuItem = ({ accessibilityLabel, + children, className, customIcon, disabled, @@ -132,6 +149,7 @@ const ContextMenuItem = ({ onClick, onKeyDown, onKeyPress, + selected, testId, text, textClassName }: IProps) => { @@ -145,6 +163,7 @@ const ContextMenuItem = ({ className = { cx(styles.contextMenuItem, _overflowDrawer && styles.contextMenuItemDrawer, disabled && styles.contextMenuItemDisabled, + selected && styles.selected, className ) } data-testid = { testId } @@ -152,13 +171,15 @@ const ContextMenuItem = ({ key = { text } onClick = { disabled ? undefined : onClick } onKeyDown = { disabled ? undefined : onKeyDown } - onKeyPress = { disabled ? undefined : onKeyPress }> + onKeyPress = { disabled ? undefined : onKeyPress } + role = 'menuitem'> {customIcon ? customIcon : icon && } - {text} + {text && {text}} + {children} ); }; diff --git a/react/features/settings/components/web/audio/AudioSettingsContent.js b/react/features/settings/components/web/audio/AudioSettingsContent.tsx similarity index 55% rename from react/features/settings/components/web/audio/AudioSettingsContent.js rename to react/features/settings/components/web/audio/AudioSettingsContent.tsx index 1638ae833..843e0d965 100644 --- a/react/features/settings/components/web/audio/AudioSettingsContent.js +++ b/react/features/settings/components/web/audio/AudioSettingsContent.tsx @@ -1,14 +1,21 @@ -// @flow - import React, { Component } from 'react'; +import { WithTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; -import { translate } from '../../../../base/i18n'; -import { IconMic, IconVolumeUp } from '../../../../base/icons'; +import { IReduxState, IStore } from '../../../../app/types'; +import { translate } from '../../../../base/i18n/functions'; +import { IconMic, IconVolumeUp } from '../../../../base/icons/svg'; import JitsiMeetJS from '../../../../base/lib-jitsi-meet'; -import { equals } from '../../../../base/redux'; -import { createLocalAudioTracks } from '../../../functions'; +import { equals } from '../../../../base/redux/functions'; +import Checkbox from '../../../../base/ui/components/web/Checkbox'; +import ContextMenu from '../../../../base/ui/components/web/ContextMenu'; +import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem'; +import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup'; +import { toggleNoiseSuppression } from '../../../../noise-suppression/actions'; +import { isNoiseSuppressionEnabled } from '../../../../noise-suppression/functions'; +import { isPrejoinPageVisible } from '../../../../prejoin/functions'; +import { createLocalAudioTracks } from '../../../functions.web'; -import AudioSettingsHeader from './AudioSettingsHeader'; import MicrophoneEntry from './MicrophoneEntry'; import SpeakerEntry from './SpeakerEntry'; @@ -22,65 +29,75 @@ const browser = JitsiMeetJS.util.browser; * @param {Function} t - The translation function. * @returns {string} */ -function transformDefaultDeviceLabel(deviceId, label, t) { +function transformDefaultDeviceLabel(deviceId: string, label: string, t: Function) { return deviceId === 'default' ? t('settings.sameAsSystem', { label: label.replace('Default - ', '') }) : label; } -export type Props = { +export interface IProps extends WithTranslation { - /** + /** * The deviceId of the microphone in use. */ - currentMicDeviceId: string, + currentMicDeviceId: string; - /** + /** * The deviceId of the output device in use. */ - currentOutputDeviceId: string, + currentOutputDeviceId?: string; /** * Used to decide whether to measure audio levels for microphone devices. */ - measureAudioLevels: boolean, + measureAudioLevels: boolean; - /** - * Used to set a new microphone as the current one. - */ - setAudioInputDevice: Function, - - /** - * Used to set a new output device as the current one. - */ - setAudioOutputDevice: Function, - - /** - * A list of objects containing the labels and deviceIds - * of all the output devices. - */ - outputDevices: Object[], - - /** + /** * A list with objects containing the labels and deviceIds * of all the input devices. */ - microphoneDevices: Object[], + microphoneDevices: Array<{ deviceId: string; label: string; }>; /** - * Invoked to obtain translated strings. + * Whether noise suppression is enabled or not. */ - t: Function -}; + noiseSuppressionEnabled: boolean; + + /** + * A list of objects containing the labels and deviceIds + * of all the output devices. + */ + outputDevices: Array<{ deviceId: string; label: string; }>; + + /** + * Whether the prejoin page is visible or not. + */ + prejoinVisible: boolean; + + /** + * Used to set a new microphone as the current one. + */ + setAudioInputDevice: Function; + + /** + * Used to set a new output device as the current one. + */ + setAudioOutputDevice: Function; + + /** + * Function to toggle noise suppression. + */ + toggleSuppression: () => void; +} type State = { - /** + /** * An list of objects, each containing the microphone label, audio track, device id * and track error if the case. */ - audioTracks: Object[] -} + audioTracks: Array<{ deviceId: string; hasError: boolean; jitsiTrack: any; label: string; }>; +}; /** * Implements a React {@link Component} which displays a list of all @@ -88,9 +105,8 @@ type State = { * * @augments Component */ -class AudioSettingsContent extends Component { +class AudioSettingsContent extends Component { _componentWasUnmounted: boolean; - _audioContentRef: Object; microphoneHeaderId = 'microphone_settings_header'; speakerHeaderId = 'speaker_settings_header'; @@ -101,13 +117,11 @@ class AudioSettingsContent extends Component { * @param {Object} props - The read-only properties with which the new * instance is to be initialized. */ - constructor(props) { + constructor(props: IProps) { super(props); this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this); this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this); - this._onEscClick = this._onEscClick.bind(this); - this._audioContentRef = React.createRef(); this.state = { audioTracks: props.microphoneDevices.map(({ deviceId, label }) => { @@ -120,23 +134,6 @@ class AudioSettingsContent extends Component { }) }; } - _onEscClick: (KeyboardEvent) => void; - - /** - * Click handler for the speaker entries. - * - * @param {KeyboardEvent} event - Esc key click to close the popup. - * @returns {void} - */ - _onEscClick(event) { - if (event.key === 'Escape') { - event.preventDefault(); - event.stopPropagation(); - this._audioContentRef.current.style.display = 'none'; - } - } - - _onMicrophoneEntryClick: (string) => void; /** * Click handler for the microphone entries. @@ -144,19 +141,17 @@ class AudioSettingsContent extends Component { * @param {string} deviceId - The deviceId for the clicked microphone. * @returns {void} */ - _onMicrophoneEntryClick(deviceId) { + _onMicrophoneEntryClick(deviceId: string) { this.props.setAudioInputDevice(deviceId); } - _onSpeakerEntryClick: (string) => void; - /** * Click handler for the speaker entries. * * @param {string} deviceId - The deviceId for the clicked speaker. * @returns {void} */ - _onSpeakerEntryClick(deviceId) { + _onSpeakerEntryClick(deviceId: string) { this.props.setAudioOutputDevice(deviceId); } @@ -169,7 +164,8 @@ class AudioSettingsContent extends Component { * @param {Function} t - The translation function. * @returns {React$Node} */ - _renderMicrophoneEntry(data, index, length, t) { + _renderMicrophoneEntry(data: { deviceId: string; hasError: boolean; jitsiTrack: any; label: string; }, + index: number, length: number, t: Function) { const { deviceId, jitsiTrack, hasError } = data; const label = transformDefaultDeviceLabel(deviceId, data.label, t); const isSelected = deviceId === this.props.currentMicDeviceId; @@ -200,7 +196,7 @@ class AudioSettingsContent extends Component { * @param {Function} t - The translation function. * @returns {React$Node} */ - _renderSpeakerEntry(data, index, length, t) { + _renderSpeakerEntry(data: { deviceId: string; label: string; }, index: number, length: number, t: Function) { const { deviceId } = data; const label = transformDefaultDeviceLabel(deviceId, data.label, t); const key = `se-${index}`; @@ -253,9 +249,9 @@ class AudioSettingsContent extends Component { * @param {Object} audioTracks - The object holding the audio tracks. * @returns {void} */ - _disposeTracks(audioTracks) { + _disposeTracks(audioTracks: Array<{ jitsiTrack: any; }>) { audioTracks.forEach(({ jitsiTrack }) => { - jitsiTrack && jitsiTrack.dispose(); + jitsiTrack?.dispose(); }); } @@ -283,7 +279,7 @@ class AudioSettingsContent extends Component { * * @inheritdoc */ - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IProps) { if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) { this._setTracks(); } @@ -296,55 +292,82 @@ class AudioSettingsContent extends Component { * @inheritdoc */ render() { - const { outputDevices, t } = this.props; + const { outputDevices, t, noiseSuppressionEnabled, toggleSuppression, prejoinVisible } = this.props; return ( -
- -
+ ) + } + {!prejoinVisible && ( + +
e.stopPropagation() }> + +
+
+ )} + ); } } -export default translate(AudioSettingsContent); +const mapStateToProps = (state: IReduxState) => { + return { + noiseSuppressionEnabled: isNoiseSuppressionEnabled(state), + prejoinVisible: isPrejoinPageVisible(state) + }; +}; + +const mapDispatchToProps = (dispatch: IStore['dispatch']) => { + return { + toggleSuppression() { + dispatch(toggleNoiseSuppression()); + } + }; +}; + +export default translate(connect(mapStateToProps, mapDispatchToProps)(AudioSettingsContent)); diff --git a/react/features/settings/components/web/audio/AudioSettingsEntry.js b/react/features/settings/components/web/audio/AudioSettingsEntry.js deleted file mode 100644 index ceeeae038..000000000 --- a/react/features/settings/components/web/audio/AudioSettingsEntry.js +++ /dev/null @@ -1,64 +0,0 @@ -// @flow - -import React from 'react'; - -import { Icon, IconCheck, IconExclamationSolid } from '../../../../base/icons'; - -/** - * The type of the React {@code Component} props of {@link AudioSettingsEntry}. - */ -export type Props = { - - /** - * The text for this component. - */ - children: React$Node, - - /** - * Flag indicating an error. - */ - hasError?: boolean, - - /** - * The id for the label, that contains the item text. - */ - labelId?: string, - - /** - * Flag indicating the selection state. - */ - isSelected: boolean, -}; - -/** - * React {@code Component} representing an entry for the audio settings. - * - * @returns { ReactElement} - */ -export default function AudioSettingsEntry( - { children, hasError, labelId, isSelected }: Props) { - - const className = `audio-preview-entry ${isSelected - ? 'audio-preview-entry--selected' : ''}`; - - return ( -
- {isSelected && ( - - )} - - {children} - - {hasError && } -
- ); -} diff --git a/react/features/settings/components/web/audio/AudioSettingsHeader.js b/react/features/settings/components/web/audio/AudioSettingsHeader.js deleted file mode 100644 index 837b58446..000000000 --- a/react/features/settings/components/web/audio/AudioSettingsHeader.js +++ /dev/null @@ -1,48 +0,0 @@ -// @flow - -import React from 'react'; - -import { Icon } from '../../../../base/icons'; - -/** - * The type of the React {@code Component} props of {@link AudioSettingsHeader}. - */ -type Props = { - - /** - * The id used for the Header-text. - */ - id?: string, - - /** - * The Icon used for the Header. - */ - IconComponent: Function, - - /** - * The text of the Header. - */ - text: string, -}; - -/** - * React {@code Component} representing the Header of an audio option group. - * - * @returns { ReactElement} - */ -export default function AudioSettingsHeader({ IconComponent, id, text }: Props) { - return ( -
-
- { } -
-
{text}
-
- ); -} diff --git a/react/features/settings/components/web/audio/AudioSettingsPopup.js b/react/features/settings/components/web/audio/AudioSettingsPopup.tsx similarity index 75% rename from react/features/settings/components/web/audio/AudioSettingsPopup.js rename to react/features/settings/components/web/audio/AudioSettingsPopup.tsx index fc9ac80f3..a1e8bf9f4 100644 --- a/react/features/settings/components/web/audio/AudioSettingsPopup.js +++ b/react/features/settings/components/web/audio/AudioSettingsPopup.tsx @@ -1,8 +1,8 @@ -// @flow +import React, { ReactNode } from 'react'; +import { connect } from 'react-redux'; -import React from 'react'; - -import { areAudioLevelsEnabled } from '../../../../base/config/functions'; +import { IReduxState } from '../../../../app/types'; +import { areAudioLevelsEnabled } from '../../../../base/config/functions.web'; import { setAudioInputDeviceAndUpdateSettings, setAudioOutputDevice as setAudioOutputDeviceAction @@ -12,39 +12,38 @@ import { getAudioOutputDeviceData } from '../../../../base/devices/functions.web'; import Popover from '../../../../base/popover/components/Popover.web'; -import { connect } from '../../../../base/redux'; import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants'; import { getCurrentMicDeviceId, getCurrentOutputDeviceId -} from '../../../../base/settings'; +} from '../../../../base/settings/functions.web'; import { toggleAudioSettings } from '../../../actions'; -import { getAudioSettingsVisibility } from '../../../functions'; +import { getAudioSettingsVisibility } from '../../../functions.web'; -import AudioSettingsContent, { type Props as AudioSettingsContentProps } from './AudioSettingsContent'; +import AudioSettingsContent, { type IProps as AudioSettingsContentProps } from './AudioSettingsContent'; -type Props = AudioSettingsContentProps & { +interface IProps extends AudioSettingsContentProps { /** * Component's children (the audio button). */ - children: React$Node, + children: ReactNode; /** * Flag controlling the visibility of the popup. */ - isOpen: boolean, + isOpen: boolean; /** * Callback executed when the popup closes. */ - onClose: Function, + onClose: Function; /** * The popup placement enum value. */ - popupPlacement: string + popupPlacement: string; } /** @@ -64,7 +63,7 @@ function AudioSettingsPopup({ outputDevices, popupPlacement, measureAudioLevels -}: Props) { +}: IProps) { return (
-1) { diff --git a/react/features/settings/components/web/audio/MicrophoneEntry.js b/react/features/settings/components/web/audio/MicrophoneEntry.tsx similarity index 73% rename from react/features/settings/components/web/audio/MicrophoneEntry.js rename to react/features/settings/components/web/audio/MicrophoneEntry.tsx index 247833082..7403ace9c 100644 --- a/react/features/settings/components/web/audio/MicrophoneEntry.js +++ b/react/features/settings/components/web/audio/MicrophoneEntry.tsx @@ -1,61 +1,80 @@ -// @flow - +import clsx from 'clsx'; import React, { Component } from 'react'; +import Icon from '../../../../base/icons/components/Icon'; +import { IconCheck, IconExclamationSolid } from '../../../../base/icons/svg'; +// eslint-disable-next-line lines-around-comment +// @ts-ignore import JitsiMeetJS from '../../../../base/lib-jitsi-meet/_'; +import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem'; -import AudioSettingsEntry, { type Props as AudioSettingsEntryProps } from './AudioSettingsEntry'; import Meter from './Meter'; const JitsiTrackEvents = JitsiMeetJS.events.track; -type Props = AudioSettingsEntryProps & { +type Props = { + + /** + * The text for this component. + */ + children: string; /** * The deviceId of the microphone. */ - deviceId: string, + deviceId: string; /** * Flag indicating if there is a problem with the device. */ - hasError?: boolean, + hasError?: boolean; /** * Flag indicating if there is a problem with the device. */ - index?: number, + index?: number; + + /** + * Flag indicating the selection state. + */ + isSelected: boolean; /** * The audio track for the current entry. */ - jitsiTrack: Object, + jitsiTrack: any; + + /** + * The id for the label, that contains the item text. + */ + labelId?: string; /** * The length of the microphone list. */ - length: number, + length: number; - /** - * Click handler for component. - */ - onClick: Function, - listHeaderId: string, + listHeaderId: string; /** * Used to decide whether to listen to audio level changes. */ - measureAudioLevels: boolean, -} + measureAudioLevels: boolean; + + /** + * Click handler for component. + */ + onClick: Function; +}; type State = { /** * The audio level. */ - level: number -} + level: number; +}; /** * React {@code Component} representing an entry for the microphone audio settings. @@ -81,8 +100,6 @@ export default class MicrophoneEntry extends Component { this._updateLevel = this._updateLevel.bind(this); } - _onClick: () => void; - /** * Click handler for the entry. * @@ -92,13 +109,6 @@ export default class MicrophoneEntry extends Component { this.props.onClick(this.props.deviceId); } - /** - * Key pressed handler for the entry. - * - * @returns {void} - */ - _onKeyPress: (KeyboardEvent) => void; - /** * Key pressed handler for the entry. * @@ -107,22 +117,20 @@ export default class MicrophoneEntry extends Component { * * @returns {void} */ - _onKeyPress(e) { + _onKeyPress(e: React.KeyboardEvent) { if (e.key === ' ') { e.preventDefault(); this.props.onClick(this.props.deviceId); } } - _updateLevel: (number) => void; - /** * Updates the level of the meter. * * @param {number} num - The audio level provided by the jitsiTrack. * @returns {void} */ - _updateLevel(num) { + _updateLevel(num: number) { this.setState({ level: Math.floor(num / 0.125) }); @@ -147,8 +155,8 @@ export default class MicrophoneEntry extends Component { * @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from. * @returns {void} */ - _stopListening(jitsiTrack) { - jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel); + _stopListening(jitsiTrack?: any) { + jitsiTrack?.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel); this.setState({ level: -1 }); @@ -202,9 +210,9 @@ export default class MicrophoneEntry extends Component { measureAudioLevels } = this.props; - const deviceTextId: string = `choose_microphone${deviceId}`; + const deviceTextId = `choose_microphone${deviceId}`; - const labelledby: string = `${listHeaderId} ${deviceTextId} `; + const labelledby = `${listHeaderId} ${deviceTextId} `; const className = `audio-preview-microphone ${measureAudioLevels ? 'audio-preview-microphone--withmeter' : 'audio-preview-microphone--nometer'}`; @@ -220,12 +228,17 @@ export default class MicrophoneEntry extends Component { onKeyPress = { this._onKeyPress } role = 'radio' tabIndex = { 0 }> - - {children} - + + {hasError && } + { Boolean(jitsiTrack) && measureAudioLevels && { - /** - * A React ref to the HTML element containing the {@code audio} instance. - */ - audioRef: Object; - - /** - * Initializes a new {@code SpeakerEntry} instance. - * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. - */ - constructor(props: Props) { - super(props); - - this.audioRef = React.createRef(); - this._onTestButtonClick = this._onTestButtonClick.bind(this); - this._onClick = this._onClick.bind(this); - this._onKeyPress = this._onKeyPress.bind(this); - } - - _onClick: () => void; - - /** - * Click handler for the entry. - * - * @returns {void} - */ - _onClick() { - this.props.onClick(this.props.deviceId); - } - - _onKeyPress: () => void; - - /** - * Key pressed handler for the entry. - * - * @param {Object} e - The event. - * @private - * - * @returns {void} - */ - _onKeyPress(e) { - if (e.key === ' ') { - e.preventDefault(); - this.props.onClick(this.props.deviceId); - } - } - - - _onTestButtonClick: Object => void; - - /** - * Click handler for Test button. - * Sets the current audio output id and plays a sound. - * - * @param {Object} e - The sythetic event. - * @returns {void} - */ - async _onTestButtonClick(e) { - e.stopPropagation(); - - try { - await this.audioRef.current.setSinkId(this.props.deviceId); - this.audioRef.current.play(); - } catch (err) { - logger.log('Could not set sink id', err); - } - } - - /** - * Implements React's {@link Component#render}. - * - * @inheritdoc - */ - render() { - const { children, isSelected, index, deviceId, length, listHeaderId } = this.props; - const deviceTextId: string = `choose_speaker${deviceId}`; - const labelledby: string = `${listHeaderId} ${deviceTextId} `; - - return ( -
  • - - {children} - - -
  • - ); - } -} diff --git a/react/features/settings/components/web/audio/SpeakerEntry.tsx b/react/features/settings/components/web/audio/SpeakerEntry.tsx new file mode 100644 index 000000000..3d0965ce0 --- /dev/null +++ b/react/features/settings/components/web/audio/SpeakerEntry.tsx @@ -0,0 +1,140 @@ +import clsx from 'clsx'; +import React, { useRef } from 'react'; + +import { IconCheck } from '../../../../base/icons/svg'; +import Button from '../../../../base/ui/components/web/Button'; +import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem'; +import { BUTTON_TYPES } from '../../../../base/ui/constants.any'; +import logger from '../../../logger'; + +const TEST_SOUND_PATH = 'sounds/ring.mp3'; + +/** + * The type of the React {@code Component} props of {@link SpeakerEntry}. + */ +interface IProps { + + /** + * The text label for the entry. + */ + children: string; + + /** + * The deviceId of the speaker. + */ + deviceId: string; + + /** + * Flag controlling the selection state of the entry. + */ + index: number; + + /** + * Flag controlling the selection state of the entry. + */ + isSelected: boolean; + + /** + * Flag controlling the selection state of the entry. + */ + length: number; + + listHeaderId: string; + + /** + * Click handler for the component. + */ + onClick: Function; +} + +/** + * Implements a React {@link Component} which displays an audio + * output settings entry. The user can click and play a test sound. + * + * @param {IProps} props - Component props. + * @returns {JSX.Element} + */ +const SpeakerEntry = (props: IProps) => { + const audioRef = useRef(null); + + /** + * Click handler for the entry. + * + * @returns {void} + */ + function _onClick() { + props.onClick(props.deviceId); + } + + /** + * Key pressed handler for the entry. + * + * @param {Object} e - The event. + * @private + * + * @returns {void} + */ + function _onKeyPress(e: React.KeyboardEvent) { + if (e.key === ' ') { + e.preventDefault(); + props.onClick(props.deviceId); + } + } + + /** + * Click handler for Test button. + * Sets the current audio output id and plays a sound. + * + * @param {Object} e - The synthetic event. + * @returns {void} + */ + async function _onTestButtonClick(e: React.KeyboardEvent | React.MouseEvent) { + e.stopPropagation(); + + try { // @ts-ignore + await audioRef.current?.setSinkId(props.deviceId); + audioRef.current?.play(); + } catch (err) { + logger.log('Could not set sink id', err); + } + } + + const { children, isSelected, index, deviceId, length, listHeaderId } = props; + const deviceTextId = `choose_speaker${deviceId}`; + const labelledby = `${listHeaderId} ${deviceTextId} `; + + /* eslint-disable react/jsx-no-bind */ + return ( +
  • + +
  • + ); +}; + + +export default SpeakerEntry; diff --git a/react/features/settings/components/web/audio/TestButton.js b/react/features/settings/components/web/audio/TestButton.js deleted file mode 100644 index 3a2f341cc..000000000 --- a/react/features/settings/components/web/audio/TestButton.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow - -import React from 'react'; - -type Props = { - - /** - * Click handler for the button. - */ - onClick: Function, - - /** - * Keypress handler for the button. - */ - onKeyPress: Function, -}; - -/** - * React {@code Component} representing an button used for testing output sound. - * - * @returns { ReactElement} - */ -export default function TestButton({ onClick, onKeyPress }: Props) { - return ( -
    - Test -
    - ); -} diff --git a/react/features/settings/functions.web.ts b/react/features/settings/functions.web.ts index d498bc8c7..a3b409480 100644 --- a/react/features/settings/functions.web.ts +++ b/react/features/settings/functions.web.ts @@ -42,7 +42,7 @@ export function createLocalVideoTracks(ids: string[], timeout?: number) { * label: string * }[]>} */ -export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) { +export function createLocalAudioTracks(devices: Array<{ deviceId: string; label: string; }>, timeout?: number) { return Promise.all( devices.map(async ({ deviceId, label }) => { let jitsiTrack = null; diff --git a/react/features/toolbox/components/web/DialogPortal.ts b/react/features/toolbox/components/web/DialogPortal.ts index e1a48381d..2c78b99cf 100644 --- a/react/features/toolbox/components/web/DialogPortal.ts +++ b/react/features/toolbox/components/web/DialogPortal.ts @@ -1,5 +1,8 @@ import { ReactNode, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; +import { useSelector } from 'react-redux'; + +import { IReduxState } from '../../../app/types'; type Props = { @@ -36,6 +39,7 @@ type Props = { * @returns {ReactElement} */ function DialogPortal({ children, className, style, getRef, setSize }: Props) { + const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth); const [ portalTarget ] = useState(() => { const portalDiv = document.createElement('div'); @@ -92,7 +96,7 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) { document.body.removeChild(portalTarget); } }; - }, []); + }, [ clientWidth ]); return ReactDOM.createPortal( children,