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
This commit is contained in:
parent
533deea5fd
commit
22ded30b61
|
@ -2,13 +2,13 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
background: $menuBG;
|
position: relative;
|
||||||
border-radius: 3px;
|
right: auto;
|
||||||
font-size: 14px;
|
margin-bottom: 8px;
|
||||||
line-height: 24px;
|
|
||||||
max-height: 456px;
|
max-height: 456px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
|
||||||
&-ul {
|
&-ul {
|
||||||
margin:0;
|
margin:0;
|
||||||
padding:0;
|
padding:0;
|
||||||
|
@ -16,90 +16,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-header {
|
&-header:hover {
|
||||||
color: #fff;
|
background-color: initial;
|
||||||
align-items: center;
|
cursor: initial;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-entry {
|
&-entry-text {
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
color: #fff;
|
text-overflow: ellipsis;
|
||||||
cursor: pointer;
|
max-width: 213px;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
padding: 8px 0;
|
white-space: nowrap;
|
||||||
margin-left: 48px;
|
|
||||||
|
|
||||||
&--selected {
|
&.left-margin {
|
||||||
background: #131519;
|
margin-left: 36px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-speaker {
|
&-speaker {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&-ul {
|
|
||||||
margin:0;
|
|
||||||
padding:0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &:focus-within, &:focus {
|
&: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 {
|
.audio-preview-test-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-preview-entry-text {
|
.audio-preview-entry-text {
|
||||||
max-width: 178px;
|
max-width: 178px;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-preview-entry-text {
|
.audio-preview-entry-text {
|
||||||
max-width: 238px;
|
max-width: 238px;
|
||||||
}
|
}
|
||||||
|
@ -108,19 +55,6 @@
|
||||||
&-microphone {
|
&-microphone {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.audio-preview-entry {
|
|
||||||
background: #36383C;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 48px;
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background: $newToolbarBackgroundColor;
|
|
||||||
padding-left: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--nometer {
|
&--nometer {
|
||||||
.audio-preview-entry-text {
|
.audio-preview-entry-text {
|
||||||
max-width: 238px;
|
max-width: 238px;
|
||||||
|
@ -140,42 +74,21 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
|
|
||||||
& svg {
|
|
||||||
fill: #1C2025;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--check {
|
|
||||||
background: #31B76A;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--exclamation {
|
&--exclamation {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
fill: #E54B4B;
|
fill: #E54B4B;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-hr {
|
|
||||||
border-top: 1px solid #4C4D50;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-test-button {
|
&-test-button {
|
||||||
display: none;
|
display: none;
|
||||||
background: #FFF;
|
padding: 4px 10px;
|
||||||
border: 1px solid #D1DBE8;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #1C2025;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 24px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
top: 5px;
|
top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-meter-mic {
|
&-meter-mic {
|
||||||
|
@ -184,9 +97,7 @@
|
||||||
top: 14px;
|
top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
&-checkbox-container {
|
||||||
& > div:nth-child(2) {
|
padding: 10px 16px;
|
||||||
outline: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,28 +3,28 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
fill: #4E5E6C;
|
fill: #525252;
|
||||||
width: 38px;
|
width: 38px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.metr--disabled {
|
&.metr--disabled {
|
||||||
& > svg {
|
& > svg {
|
||||||
fill: #4E5E6C;
|
fill: #525252;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metr-l-0 {
|
.metr-l-0 {
|
||||||
rect:first-child {
|
rect:first-child {
|
||||||
fill: #31B76A;
|
fill: #1EC26A;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 through 7 {
|
@for $i from 1 through 7 {
|
||||||
.metr-l-#{$i} {
|
.metr-l-#{$i} {
|
||||||
rect:nth-child(-n+#{$i+1}) {
|
rect:nth-child(-n+#{$i+1}) {
|
||||||
fill: #31B76A;
|
fill: #1EC26A;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<svg width="38" height="12" viewBox="0 0 38 12" fill="#5E6D7A" xmlns="http://www.w3.org/2000/svg">
|
<svg width="38" height="12" viewBox="0 0 38 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="3" height="12" rx="1"/>
|
<rect width="3" height="12" rx="1" />
|
||||||
<rect x="5" width="3" height="12" rx="1" />
|
<rect x="5" width="3" height="12" rx="1" />
|
||||||
<rect x="10" width="3" height="12" rx="1" />
|
<rect x="10" width="3" height="12" rx="1" />
|
||||||
<rect x="15" width="3" height="12" rx="1" />
|
<rect x="15" width="3" height="12" rx="1" />
|
||||||
<rect x="20" width="3" height="12" rx="1" />
|
<rect x="20" width="3" height="12" rx="1" />
|
||||||
<rect x="25" width="3" height="12" rx="1" />
|
<rect x="25" width="3" height="12" rx="1" />
|
||||||
<rect x="30" width="3" height="12" rx="1" />
|
<rect x="30" width="3" height="12" rx="1" />
|
||||||
<rect x="35" width="3" height="12" rx="1" />
|
<rect x="35" width="3" height="12" rx="1" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 487 B |
|
@ -52,7 +52,7 @@ const DEFAULT_STATE: ISettingsState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ISettingsState {
|
export interface ISettingsState {
|
||||||
audioOutputDeviceId?: string | boolean;
|
audioOutputDeviceId?: string;
|
||||||
audioSettingsVisible?: boolean;
|
audioSettingsVisible?: boolean;
|
||||||
avatarURL?: string;
|
avatarURL?: string;
|
||||||
cameraDeviceId?: string | boolean;
|
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
|
// we want to filter these props, to not be stored as they represent
|
||||||
// what is currently opened/used as devices
|
// what is currently opened/used as devices
|
||||||
|
// @ts-ignore
|
||||||
filterSubtree.audioOutputDeviceId = false;
|
filterSubtree.audioOutputDeviceId = false;
|
||||||
filterSubtree.cameraDeviceId = false;
|
filterSubtree.cameraDeviceId = false;
|
||||||
filterSubtree.micDeviceId = false;
|
filterSubtree.micDeviceId = false;
|
||||||
|
|
|
@ -34,6 +34,8 @@ const getComputedOuterHeight = (element: HTMLElement) => {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
||||||
|
[key: `aria-${string}`]: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessibility label for menu container.
|
* Accessibility label for menu container.
|
||||||
*/
|
*/
|
||||||
|
@ -59,6 +61,11 @@ interface IProps {
|
||||||
*/
|
*/
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional id.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the menu is already in a drawer.
|
* Whether or not the menu is already in a drawer.
|
||||||
*/
|
*/
|
||||||
|
@ -98,6 +105,11 @@ interface IProps {
|
||||||
* Callback for the mouse leaving the component.
|
* Callback for the mouse leaving the component.
|
||||||
*/
|
*/
|
||||||
onMouseLeave?: (e?: React.MouseEvent) => void;
|
onMouseLeave?: (e?: React.MouseEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab index for the menu.
|
||||||
|
*/
|
||||||
|
tabIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_HEIGHT = 400;
|
const MAX_HEIGHT = 400;
|
||||||
|
@ -146,6 +158,7 @@ const ContextMenu = ({
|
||||||
className,
|
className,
|
||||||
entity,
|
entity,
|
||||||
hidden,
|
hidden,
|
||||||
|
id,
|
||||||
inDrawer,
|
inDrawer,
|
||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
offsetTarget,
|
offsetTarget,
|
||||||
|
@ -153,7 +166,8 @@ const ContextMenu = ({
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onDrawerClose,
|
onDrawerClose,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave
|
onMouseLeave,
|
||||||
|
tabIndex
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
const [ isHidden, setIsHidden ] = useState(true);
|
const [ isHidden, setIsHidden ] = useState(true);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
@ -217,11 +231,14 @@ const ContextMenu = ({
|
||||||
isHidden && styles.contextMenuHidden,
|
isHidden && styles.contextMenuHidden,
|
||||||
className
|
className
|
||||||
) }
|
) }
|
||||||
|
id = { id }
|
||||||
onClick = { onClick }
|
onClick = { onClick }
|
||||||
onKeyDown = { onKeyDown }
|
onKeyDown = { onKeyDown }
|
||||||
onMouseEnter = { onMouseEnter }
|
onMouseEnter = { onMouseEnter }
|
||||||
onMouseLeave = { onMouseLeave }
|
onMouseLeave = { onMouseLeave }
|
||||||
ref = { containerRef }>
|
ref = { containerRef }
|
||||||
|
role = 'menu'
|
||||||
|
tabIndex = { tabIndex }>
|
||||||
{children}
|
{children}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,6 +13,11 @@ export interface IProps {
|
||||||
*/
|
*/
|
||||||
accessibilityLabel: string;
|
accessibilityLabel: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component children.
|
||||||
|
*/
|
||||||
|
children?: ReactNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSS class name used for custom styles.
|
* CSS class name used for custom styles.
|
||||||
*/
|
*/
|
||||||
|
@ -54,6 +59,11 @@ export interface IProps {
|
||||||
*/
|
*/
|
||||||
onKeyPress?: (e?: React.KeyboardEvent) => void;
|
onKeyPress?: (e?: React.KeyboardEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the item is marked as selected.
|
||||||
|
*/
|
||||||
|
selected?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TestId of the element, if any.
|
* TestId of the element, if any.
|
||||||
*/
|
*/
|
||||||
|
@ -62,7 +72,7 @@ export interface IProps {
|
||||||
/**
|
/**
|
||||||
* Action text.
|
* Action text.
|
||||||
*/
|
*/
|
||||||
text: string;
|
text?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class name for the text.
|
* 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: {
|
contextMenuItemDisabled: {
|
||||||
pointerEvents: 'none'
|
pointerEvents: 'none'
|
||||||
},
|
},
|
||||||
|
@ -124,6 +140,7 @@ const useStyles = makeStyles()(theme => {
|
||||||
|
|
||||||
const ContextMenuItem = ({
|
const ContextMenuItem = ({
|
||||||
accessibilityLabel,
|
accessibilityLabel,
|
||||||
|
children,
|
||||||
className,
|
className,
|
||||||
customIcon,
|
customIcon,
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -132,6 +149,7 @@ const ContextMenuItem = ({
|
||||||
onClick,
|
onClick,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onKeyPress,
|
onKeyPress,
|
||||||
|
selected,
|
||||||
testId,
|
testId,
|
||||||
text,
|
text,
|
||||||
textClassName }: IProps) => {
|
textClassName }: IProps) => {
|
||||||
|
@ -145,6 +163,7 @@ const ContextMenuItem = ({
|
||||||
className = { cx(styles.contextMenuItem,
|
className = { cx(styles.contextMenuItem,
|
||||||
_overflowDrawer && styles.contextMenuItemDrawer,
|
_overflowDrawer && styles.contextMenuItemDrawer,
|
||||||
disabled && styles.contextMenuItemDisabled,
|
disabled && styles.contextMenuItemDisabled,
|
||||||
|
selected && styles.selected,
|
||||||
className
|
className
|
||||||
) }
|
) }
|
||||||
data-testid = { testId }
|
data-testid = { testId }
|
||||||
|
@ -152,13 +171,15 @@ const ContextMenuItem = ({
|
||||||
key = { text }
|
key = { text }
|
||||||
onClick = { disabled ? undefined : onClick }
|
onClick = { disabled ? undefined : onClick }
|
||||||
onKeyDown = { disabled ? undefined : onKeyDown }
|
onKeyDown = { disabled ? undefined : onKeyDown }
|
||||||
onKeyPress = { disabled ? undefined : onKeyPress }>
|
onKeyPress = { disabled ? undefined : onKeyPress }
|
||||||
|
role = 'menuitem'>
|
||||||
{customIcon ? customIcon
|
{customIcon ? customIcon
|
||||||
: icon && <Icon
|
: icon && <Icon
|
||||||
className = { styles.contextMenuItemIcon }
|
className = { styles.contextMenuItemIcon }
|
||||||
size = { 20 }
|
size = { 20 }
|
||||||
src = { icon } />}
|
src = { icon } />}
|
||||||
<span className = { cx(styles.text, textClassName) }>{text}</span>
|
{text && <span className = { cx(styles.text, textClassName) }>{text}</span>}
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { WithTranslation } from 'react-i18next';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../../../base/i18n';
|
import { IReduxState, IStore } from '../../../../app/types';
|
||||||
import { IconMic, IconVolumeUp } from '../../../../base/icons';
|
import { translate } from '../../../../base/i18n/functions';
|
||||||
|
import { IconMic, IconVolumeUp } from '../../../../base/icons/svg';
|
||||||
import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
|
import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
|
||||||
import { equals } from '../../../../base/redux';
|
import { equals } from '../../../../base/redux/functions';
|
||||||
import { createLocalAudioTracks } from '../../../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 MicrophoneEntry from './MicrophoneEntry';
|
||||||
import SpeakerEntry from './SpeakerEntry';
|
import SpeakerEntry from './SpeakerEntry';
|
||||||
|
|
||||||
|
@ -22,65 +29,75 @@ const browser = JitsiMeetJS.util.browser;
|
||||||
* @param {Function} t - The translation function.
|
* @param {Function} t - The translation function.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function transformDefaultDeviceLabel(deviceId, label, t) {
|
function transformDefaultDeviceLabel(deviceId: string, label: string, t: Function) {
|
||||||
return deviceId === 'default'
|
return deviceId === 'default'
|
||||||
? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
|
? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
|
||||||
: label;
|
: label;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Props = {
|
export interface IProps extends WithTranslation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The deviceId of the microphone in use.
|
* The deviceId of the microphone in use.
|
||||||
*/
|
*/
|
||||||
currentMicDeviceId: string,
|
currentMicDeviceId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The deviceId of the output device in use.
|
* The deviceId of the output device in use.
|
||||||
*/
|
*/
|
||||||
currentOutputDeviceId: string,
|
currentOutputDeviceId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to decide whether to measure audio levels for microphone devices.
|
* 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
|
* A list with objects containing the labels and deviceIds
|
||||||
* of all the input devices.
|
* 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 = {
|
type State = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An list of objects, each containing the microphone label, audio track, device id
|
* An list of objects, each containing the microphone label, audio track, device id
|
||||||
* and track error if the case.
|
* 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
|
* Implements a React {@link Component} which displays a list of all
|
||||||
|
@ -88,9 +105,8 @@ type State = {
|
||||||
*
|
*
|
||||||
* @augments Component
|
* @augments Component
|
||||||
*/
|
*/
|
||||||
class AudioSettingsContent extends Component<Props, State> {
|
class AudioSettingsContent extends Component<IProps, State> {
|
||||||
_componentWasUnmounted: boolean;
|
_componentWasUnmounted: boolean;
|
||||||
_audioContentRef: Object;
|
|
||||||
microphoneHeaderId = 'microphone_settings_header';
|
microphoneHeaderId = 'microphone_settings_header';
|
||||||
speakerHeaderId = 'speaker_settings_header';
|
speakerHeaderId = 'speaker_settings_header';
|
||||||
|
|
||||||
|
@ -101,13 +117,11 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
* @param {Object} props - The read-only properties with which the new
|
* @param {Object} props - The read-only properties with which the new
|
||||||
* instance is to be initialized.
|
* instance is to be initialized.
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this);
|
this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this);
|
||||||
this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this);
|
this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this);
|
||||||
this._onEscClick = this._onEscClick.bind(this);
|
|
||||||
this._audioContentRef = React.createRef();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
audioTracks: props.microphoneDevices.map(({ deviceId, label }) => {
|
audioTracks: props.microphoneDevices.map(({ deviceId, label }) => {
|
||||||
|
@ -120,23 +134,6 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_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.
|
* Click handler for the microphone entries.
|
||||||
|
@ -144,19 +141,17 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
* @param {string} deviceId - The deviceId for the clicked microphone.
|
* @param {string} deviceId - The deviceId for the clicked microphone.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onMicrophoneEntryClick(deviceId) {
|
_onMicrophoneEntryClick(deviceId: string) {
|
||||||
this.props.setAudioInputDevice(deviceId);
|
this.props.setAudioInputDevice(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSpeakerEntryClick: (string) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click handler for the speaker entries.
|
* Click handler for the speaker entries.
|
||||||
*
|
*
|
||||||
* @param {string} deviceId - The deviceId for the clicked speaker.
|
* @param {string} deviceId - The deviceId for the clicked speaker.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onSpeakerEntryClick(deviceId) {
|
_onSpeakerEntryClick(deviceId: string) {
|
||||||
this.props.setAudioOutputDevice(deviceId);
|
this.props.setAudioOutputDevice(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +164,8 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
* @param {Function} t - The translation function.
|
* @param {Function} t - The translation function.
|
||||||
* @returns {React$Node}
|
* @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 { deviceId, jitsiTrack, hasError } = data;
|
||||||
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
|
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
|
||||||
const isSelected = deviceId === this.props.currentMicDeviceId;
|
const isSelected = deviceId === this.props.currentMicDeviceId;
|
||||||
|
@ -200,7 +196,7 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
* @param {Function} t - The translation function.
|
* @param {Function} t - The translation function.
|
||||||
* @returns {React$Node}
|
* @returns {React$Node}
|
||||||
*/
|
*/
|
||||||
_renderSpeakerEntry(data, index, length, t) {
|
_renderSpeakerEntry(data: { deviceId: string; label: string; }, index: number, length: number, t: Function) {
|
||||||
const { deviceId } = data;
|
const { deviceId } = data;
|
||||||
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
|
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
|
||||||
const key = `se-${index}`;
|
const key = `se-${index}`;
|
||||||
|
@ -253,9 +249,9 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
* @param {Object} audioTracks - The object holding the audio tracks.
|
* @param {Object} audioTracks - The object holding the audio tracks.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_disposeTracks(audioTracks) {
|
_disposeTracks(audioTracks: Array<{ jitsiTrack: any; }>) {
|
||||||
audioTracks.forEach(({ jitsiTrack }) => {
|
audioTracks.forEach(({ jitsiTrack }) => {
|
||||||
jitsiTrack && jitsiTrack.dispose();
|
jitsiTrack?.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +279,7 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps: IProps) {
|
||||||
if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) {
|
if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) {
|
||||||
this._setTracks();
|
this._setTracks();
|
||||||
}
|
}
|
||||||
|
@ -296,55 +292,82 @@ class AudioSettingsContent extends Component<Props, State> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { outputDevices, t } = this.props;
|
const { outputDevices, t, noiseSuppressionEnabled, toggleSuppression, prejoinVisible } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ContextMenu
|
||||||
<div
|
aria-labelledby = 'audio-settings-button'
|
||||||
aria-labelledby = 'audio-settings-button'
|
className = 'audio-preview-content'
|
||||||
className = 'audio-preview-content'
|
hidden = { false }
|
||||||
id = 'audio-settings-dialog'
|
id = 'audio-settings-dialog'
|
||||||
onKeyDown = { this._onEscClick }
|
tabIndex = { -1 }>
|
||||||
ref = { this._audioContentRef }
|
<ContextMenuItemGroup>
|
||||||
role = 'menu'
|
<ContextMenuItem
|
||||||
tabIndex = { -1 }>
|
accessibilityLabel = { t('settings.microphones') }
|
||||||
<div role = 'menuitem'>
|
className = 'audio-preview-header'
|
||||||
<AudioSettingsHeader
|
icon = { IconMic }
|
||||||
IconComponent = { IconMic }
|
id = { this.microphoneHeaderId }
|
||||||
id = { this.microphoneHeaderId }
|
text = { t('settings.microphones') } />
|
||||||
text = { t('settings.microphones') } />
|
<ul
|
||||||
|
aria-labelledby = { this.microphoneHeaderId }
|
||||||
|
className = 'audio-preview-content-ul'
|
||||||
|
role = 'radiogroup'
|
||||||
|
tabIndex = { -1 }>
|
||||||
|
{this.state.audioTracks.map((data, i) =>
|
||||||
|
this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</ContextMenuItemGroup>
|
||||||
|
{ outputDevices.length > 0 && (
|
||||||
|
<ContextMenuItemGroup>
|
||||||
|
<ContextMenuItem
|
||||||
|
accessibilityLabel = { t('settings.speakers') }
|
||||||
|
className = 'audio-preview-header'
|
||||||
|
icon = { IconVolumeUp }
|
||||||
|
id = { this.speakerHeaderId }
|
||||||
|
text = { t('settings.speakers') } />
|
||||||
<ul
|
<ul
|
||||||
aria-labelledby = 'microphone_settings_header'
|
aria-labelledby = { this.speakerHeaderId }
|
||||||
className = 'audio-preview-content-ul'
|
className = 'audio-preview-content-ul'
|
||||||
role = 'radiogroup'
|
role = 'radiogroup'
|
||||||
tabIndex = '-1'>
|
tabIndex = { -1 }>
|
||||||
{this.state.audioTracks.map((data, i) =>
|
{ outputDevices.map((data, i) =>
|
||||||
this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t)
|
this._renderSpeakerEntry(data, i, outputDevices.length, t)
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</ContextMenuItemGroup>)
|
||||||
{ outputDevices.length > 0 && (
|
}
|
||||||
<div role = 'menuitem'>
|
{!prejoinVisible && (
|
||||||
<hr className = 'audio-preview-hr' />
|
<ContextMenuItemGroup>
|
||||||
<AudioSettingsHeader
|
<div
|
||||||
IconComponent = { IconVolumeUp }
|
className = 'audio-preview-checkbox-container'
|
||||||
id = { this.speakerHeaderId }
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
text = { t('settings.speakers') } />
|
onClick = { e => e.stopPropagation() }>
|
||||||
<ul
|
<Checkbox
|
||||||
aria-labelledby = 'speaker_settings_header'
|
checked = { noiseSuppressionEnabled }
|
||||||
className = 'audio-preview-content-ul'
|
label = { t('toolbar.noiseSuppression') }
|
||||||
role = 'radiogroup'
|
onChange = { toggleSuppression } />
|
||||||
tabIndex = '-1'>
|
</div>
|
||||||
{ outputDevices.map((data, i) =>
|
</ContextMenuItemGroup>
|
||||||
this._renderSpeakerEntry(data, i, outputDevices.length, t)
|
)}
|
||||||
)}
|
</ContextMenu>
|
||||||
</ul>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
|
@ -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 (
|
|
||||||
<div className = { className }>
|
|
||||||
{isSelected && (
|
|
||||||
<Icon
|
|
||||||
className = 'audio-preview-icon audio-preview-icon--check'
|
|
||||||
color = '#1C2025'
|
|
||||||
size = { 14 }
|
|
||||||
src = { IconCheck } />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className = 'audio-preview-entry-text'
|
|
||||||
id = { labelId }>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
{hasError && <Icon
|
|
||||||
className = 'audio-preview-icon audio-preview-icon--exclamation'
|
|
||||||
size = { 16 }
|
|
||||||
src = { IconExclamationSolid } />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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 (
|
|
||||||
<div
|
|
||||||
className = 'audio-preview-header'
|
|
||||||
role = 'heading'>
|
|
||||||
<div className = 'audio-preview-header-icon'>
|
|
||||||
{ <Icon
|
|
||||||
size = { 20 }
|
|
||||||
src = { IconComponent } />}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className = 'audio-preview-header-text'
|
|
||||||
id = { id } >{text}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
import React, { ReactNode } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import React from 'react';
|
import { IReduxState } from '../../../../app/types';
|
||||||
|
import { areAudioLevelsEnabled } from '../../../../base/config/functions.web';
|
||||||
import { areAudioLevelsEnabled } from '../../../../base/config/functions';
|
|
||||||
import {
|
import {
|
||||||
setAudioInputDeviceAndUpdateSettings,
|
setAudioInputDeviceAndUpdateSettings,
|
||||||
setAudioOutputDevice as setAudioOutputDeviceAction
|
setAudioOutputDevice as setAudioOutputDeviceAction
|
||||||
|
@ -12,39 +12,38 @@ import {
|
||||||
getAudioOutputDeviceData
|
getAudioOutputDeviceData
|
||||||
} from '../../../../base/devices/functions.web';
|
} from '../../../../base/devices/functions.web';
|
||||||
import Popover from '../../../../base/popover/components/Popover.web';
|
import Popover from '../../../../base/popover/components/Popover.web';
|
||||||
import { connect } from '../../../../base/redux';
|
|
||||||
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
|
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
|
||||||
import {
|
import {
|
||||||
getCurrentMicDeviceId,
|
getCurrentMicDeviceId,
|
||||||
getCurrentOutputDeviceId
|
getCurrentOutputDeviceId
|
||||||
} from '../../../../base/settings';
|
} from '../../../../base/settings/functions.web';
|
||||||
import { toggleAudioSettings } from '../../../actions';
|
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).
|
* Component's children (the audio button).
|
||||||
*/
|
*/
|
||||||
children: React$Node,
|
children: ReactNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag controlling the visibility of the popup.
|
* Flag controlling the visibility of the popup.
|
||||||
*/
|
*/
|
||||||
isOpen: boolean,
|
isOpen: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback executed when the popup closes.
|
* Callback executed when the popup closes.
|
||||||
*/
|
*/
|
||||||
onClose: Function,
|
onClose: Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The popup placement enum value.
|
* The popup placement enum value.
|
||||||
*/
|
*/
|
||||||
popupPlacement: string
|
popupPlacement: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,7 +63,7 @@ function AudioSettingsPopup({
|
||||||
outputDevices,
|
outputDevices,
|
||||||
popupPlacement,
|
popupPlacement,
|
||||||
measureAudioLevels
|
measureAudioLevels
|
||||||
}: Props) {
|
}: IProps) {
|
||||||
return (
|
return (
|
||||||
<div className = 'audio-preview'>
|
<div className = 'audio-preview'>
|
||||||
<Popover
|
<Popover
|
||||||
|
@ -92,16 +91,16 @@ function AudioSettingsPopup({
|
||||||
* @param {Object} state - Redux state.
|
* @param {Object} state - Redux state.
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state: IReduxState) {
|
||||||
const { clientWidth } = state['features/base/responsive-ui'];
|
const { clientWidth } = state['features/base/responsive-ui'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
popupPlacement: clientWidth <= SMALL_MOBILE_WIDTH ? 'auto' : 'top-end',
|
popupPlacement: clientWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
|
||||||
currentMicDeviceId: getCurrentMicDeviceId(state),
|
currentMicDeviceId: getCurrentMicDeviceId(state),
|
||||||
currentOutputDeviceId: getCurrentOutputDeviceId(state),
|
currentOutputDeviceId: getCurrentOutputDeviceId(state),
|
||||||
isOpen: getAudioSettingsVisibility(state),
|
isOpen: Boolean(getAudioSettingsVisibility(state)),
|
||||||
microphoneDevices: getAudioInputDeviceData(state),
|
microphoneDevices: getAudioInputDeviceData(state) ?? [],
|
||||||
outputDevices: getAudioOutputDeviceData(state),
|
outputDevices: getAudioOutputDeviceData(state) ?? [],
|
||||||
measureAudioLevels: areAudioLevelsEnabled(state)
|
measureAudioLevels: areAudioLevelsEnabled(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,34 +1,33 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Icon, IconMeter } from '../../../../base/icons';
|
import Icon from '../../../../base/icons/components/Icon';
|
||||||
|
import { IconMeter } from '../../../../base/icons/svg';
|
||||||
|
|
||||||
type Props = {
|
interface IProps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Own class name for the component.
|
* Own class name for the component.
|
||||||
*/
|
*/
|
||||||
className: string,
|
className: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag indicating whether the component is greyed out/disabled.
|
* Flag indicating whether the component is greyed out/disabled.
|
||||||
*/
|
*/
|
||||||
isDisabled?: boolean,
|
isDisabled?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The level of the meter.
|
* The level of the meter.
|
||||||
* Should be between 0 and 7 as per the used SVG.
|
* Should be between 0 and 7 as per the used SVG.
|
||||||
*/
|
*/
|
||||||
level: number,
|
level: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React {@code Component} representing an audio level meter.
|
* React {@code Component} representing an audio level meter.
|
||||||
*
|
*
|
||||||
* @returns { ReactElement}
|
* @returns { ReactElement}
|
||||||
*/
|
*/
|
||||||
export default function({ className, isDisabled, level }: Props) {
|
export default function({ className, isDisabled, level }: IProps) {
|
||||||
let ownClassName;
|
let ownClassName;
|
||||||
|
|
||||||
if (level > -1) {
|
if (level > -1) {
|
|
@ -1,61 +1,80 @@
|
||||||
// @flow
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
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 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';
|
import Meter from './Meter';
|
||||||
|
|
||||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||||
|
|
||||||
type Props = AudioSettingsEntryProps & {
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text for this component.
|
||||||
|
*/
|
||||||
|
children: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The deviceId of the microphone.
|
* The deviceId of the microphone.
|
||||||
*/
|
*/
|
||||||
deviceId: string,
|
deviceId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag indicating if there is a problem with the device.
|
* Flag indicating if there is a problem with the device.
|
||||||
*/
|
*/
|
||||||
hasError?: boolean,
|
hasError?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag indicating if there is a problem with the device.
|
* 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.
|
* 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.
|
* The length of the microphone list.
|
||||||
*/
|
*/
|
||||||
length: number,
|
length: number;
|
||||||
|
|
||||||
|
|
||||||
/**
|
listHeaderId: string;
|
||||||
* Click handler for component.
|
|
||||||
*/
|
|
||||||
onClick: Function,
|
|
||||||
listHeaderId: string,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to decide whether to listen to audio level changes.
|
* Used to decide whether to listen to audio level changes.
|
||||||
*/
|
*/
|
||||||
measureAudioLevels: boolean,
|
measureAudioLevels: boolean;
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Click handler for component.
|
||||||
|
*/
|
||||||
|
onClick: Function;
|
||||||
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The audio level.
|
* The audio level.
|
||||||
*/
|
*/
|
||||||
level: number
|
level: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React {@code Component} representing an entry for the microphone audio settings.
|
* React {@code Component} representing an entry for the microphone audio settings.
|
||||||
|
@ -81,8 +100,6 @@ export default class MicrophoneEntry extends Component<Props, State> {
|
||||||
this._updateLevel = this._updateLevel.bind(this);
|
this._updateLevel = this._updateLevel.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClick: () => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click handler for the entry.
|
* Click handler for the entry.
|
||||||
*
|
*
|
||||||
|
@ -92,13 +109,6 @@ export default class MicrophoneEntry extends Component<Props, State> {
|
||||||
this.props.onClick(this.props.deviceId);
|
this.props.onClick(this.props.deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Key pressed handler for the entry.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_onKeyPress: (KeyboardEvent) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key pressed handler for the entry.
|
* Key pressed handler for the entry.
|
||||||
*
|
*
|
||||||
|
@ -107,22 +117,20 @@ export default class MicrophoneEntry extends Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onKeyPress(e) {
|
_onKeyPress(e: React.KeyboardEvent) {
|
||||||
if (e.key === ' ') {
|
if (e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onClick(this.props.deviceId);
|
this.props.onClick(this.props.deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateLevel: (number) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the level of the meter.
|
* Updates the level of the meter.
|
||||||
*
|
*
|
||||||
* @param {number} num - The audio level provided by the jitsiTrack.
|
* @param {number} num - The audio level provided by the jitsiTrack.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_updateLevel(num) {
|
_updateLevel(num: number) {
|
||||||
this.setState({
|
this.setState({
|
||||||
level: Math.floor(num / 0.125)
|
level: Math.floor(num / 0.125)
|
||||||
});
|
});
|
||||||
|
@ -147,8 +155,8 @@ export default class MicrophoneEntry extends Component<Props, State> {
|
||||||
* @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from.
|
* @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_stopListening(jitsiTrack) {
|
_stopListening(jitsiTrack?: any) {
|
||||||
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel);
|
jitsiTrack?.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel);
|
||||||
this.setState({
|
this.setState({
|
||||||
level: -1
|
level: -1
|
||||||
});
|
});
|
||||||
|
@ -202,9 +210,9 @@ export default class MicrophoneEntry extends Component<Props, State> {
|
||||||
measureAudioLevels
|
measureAudioLevels
|
||||||
} = this.props;
|
} = 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
|
const className = `audio-preview-microphone ${measureAudioLevels
|
||||||
? 'audio-preview-microphone--withmeter' : 'audio-preview-microphone--nometer'}`;
|
? 'audio-preview-microphone--withmeter' : 'audio-preview-microphone--nometer'}`;
|
||||||
|
@ -220,12 +228,17 @@ export default class MicrophoneEntry extends Component<Props, State> {
|
||||||
onKeyPress = { this._onKeyPress }
|
onKeyPress = { this._onKeyPress }
|
||||||
role = 'radio'
|
role = 'radio'
|
||||||
tabIndex = { 0 }>
|
tabIndex = { 0 }>
|
||||||
<AudioSettingsEntry
|
<ContextMenuItem
|
||||||
hasError = { hasError }
|
accessibilityLabel = ''
|
||||||
isSelected = { isSelected }
|
icon = { isSelected ? IconCheck : undefined }
|
||||||
labelId = { deviceTextId }>
|
selected = { isSelected }
|
||||||
{children}
|
text = { children }
|
||||||
</AudioSettingsEntry>
|
textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }>
|
||||||
|
{hasError && <Icon
|
||||||
|
className = 'audio-preview-icon audio-preview-icon--exclamation'
|
||||||
|
size = { 16 }
|
||||||
|
src = { IconExclamationSolid } />}
|
||||||
|
</ContextMenuItem>
|
||||||
{ Boolean(jitsiTrack) && measureAudioLevels && <Meter
|
{ Boolean(jitsiTrack) && measureAudioLevels && <Meter
|
||||||
className = 'audio-preview-meter-mic'
|
className = 'audio-preview-meter-mic'
|
||||||
isDisabled = { hasError }
|
isDisabled = { hasError }
|
|
@ -1,163 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import logger from '../../../logger';
|
|
||||||
|
|
||||||
import AudioSettingsEntry from './AudioSettingsEntry';
|
|
||||||
import TestButton from './TestButton';
|
|
||||||
|
|
||||||
const TEST_SOUND_PATH = 'sounds/ring.mp3';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of the React {@code Component} props of {@link SpeakerEntry}.
|
|
||||||
*/
|
|
||||||
type Props = {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The text label for the entry.
|
|
||||||
*/
|
|
||||||
children: React$Node,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag controlling the selection state of the entry.
|
|
||||||
*/
|
|
||||||
isSelected: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag controlling the selection state of the entry.
|
|
||||||
*/
|
|
||||||
index: number,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag controlling the selection state of the entry.
|
|
||||||
*/
|
|
||||||
length: number,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The deviceId of the speaker.
|
|
||||||
*/
|
|
||||||
deviceId: string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click handler for the component.
|
|
||||||
*/
|
|
||||||
onClick: Function,
|
|
||||||
listHeaderId: string
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements a React {@link Component} which displays an audio
|
|
||||||
* output settings entry. The user can click and play a test sound.
|
|
||||||
*
|
|
||||||
* @augments Component
|
|
||||||
*/
|
|
||||||
export default class SpeakerEntry extends Component<Props> {
|
|
||||||
/**
|
|
||||||
* 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 (
|
|
||||||
<li
|
|
||||||
aria-checked = { isSelected }
|
|
||||||
aria-labelledby = { labelledby }
|
|
||||||
aria-posinset = { index }
|
|
||||||
aria-setsize = { length }
|
|
||||||
className = 'audio-preview-speaker'
|
|
||||||
onClick = { this._onClick }
|
|
||||||
onKeyPress = { this._onKeyPress }
|
|
||||||
role = 'radio'
|
|
||||||
tabIndex = { 0 }>
|
|
||||||
<AudioSettingsEntry
|
|
||||||
isSelected = { isSelected }
|
|
||||||
key = { deviceId }
|
|
||||||
labelId = { deviceTextId }>
|
|
||||||
{children}
|
|
||||||
</AudioSettingsEntry>
|
|
||||||
<TestButton
|
|
||||||
onClick = { this._onTestButtonClick }
|
|
||||||
onKeyPress = { this._onTestButtonClick } />
|
|
||||||
<audio
|
|
||||||
preload = 'auto'
|
|
||||||
ref = { this.audioRef }
|
|
||||||
src = { TEST_SOUND_PATH } />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<HTMLAudioElement | null>(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 (
|
||||||
|
<li
|
||||||
|
aria-checked = { isSelected }
|
||||||
|
aria-labelledby = { labelledby }
|
||||||
|
aria-posinset = { index }
|
||||||
|
aria-setsize = { length }
|
||||||
|
className = 'audio-preview-speaker'
|
||||||
|
onClick = { _onClick }
|
||||||
|
onKeyPress = { _onKeyPress }
|
||||||
|
role = 'radio'
|
||||||
|
tabIndex = { 0 }>
|
||||||
|
<ContextMenuItem
|
||||||
|
accessibilityLabel = ''
|
||||||
|
icon = { isSelected ? IconCheck : undefined }
|
||||||
|
selected = { isSelected }
|
||||||
|
text = { children }
|
||||||
|
textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }>
|
||||||
|
<Button
|
||||||
|
className = 'audio-preview-test-button'
|
||||||
|
label = 'Test'
|
||||||
|
onClick = { _onTestButtonClick }
|
||||||
|
onKeyPress = { _onTestButtonClick }
|
||||||
|
type = { BUTTON_TYPES.SECONDARY } />
|
||||||
|
</ContextMenuItem>
|
||||||
|
<audio
|
||||||
|
preload = 'auto'
|
||||||
|
ref = { audioRef }
|
||||||
|
src = { TEST_SOUND_PATH } />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default SpeakerEntry;
|
|
@ -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 (
|
|
||||||
<div
|
|
||||||
className = 'audio-preview-test-button'
|
|
||||||
onClick = { onClick }
|
|
||||||
onKeyPress = { onKeyPress }
|
|
||||||
role = 'button'
|
|
||||||
tabIndex = { 0 }>
|
|
||||||
Test
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -42,7 +42,7 @@ export function createLocalVideoTracks(ids: string[], timeout?: number) {
|
||||||
* label: string
|
* label: string
|
||||||
* }[]>}
|
* }[]>}
|
||||||
*/
|
*/
|
||||||
export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) {
|
export function createLocalAudioTracks(devices: Array<{ deviceId: string; label: string; }>, timeout?: number) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
devices.map(async ({ deviceId, label }) => {
|
devices.map(async ({ deviceId, label }) => {
|
||||||
let jitsiTrack = null;
|
let jitsiTrack = null;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { IReduxState } from '../../../app/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
@ -36,6 +39,7 @@ type Props = {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
function DialogPortal({ children, className, style, getRef, setSize }: Props) {
|
function DialogPortal({ children, className, style, getRef, setSize }: Props) {
|
||||||
|
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
|
||||||
const [ portalTarget ] = useState(() => {
|
const [ portalTarget ] = useState(() => {
|
||||||
const portalDiv = document.createElement('div');
|
const portalDiv = document.createElement('div');
|
||||||
|
|
||||||
|
@ -92,7 +96,7 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) {
|
||||||
document.body.removeChild(portalTarget);
|
document.body.removeChild(portalTarget);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [ clientWidth ]);
|
||||||
|
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
children,
|
children,
|
||||||
|
|
Loading…
Reference in New Issue