Compare commits

..

1 Commits

Author SHA1 Message Date
damencho
3ff71dc06a fix: Drop unused dependency. 2023-02-12 16:08:45 -06:00
24 changed files with 678 additions and 468 deletions

View File

@@ -2,13 +2,13 @@
display: inline-block;
&-content {
position: relative;
right: auto;
margin-bottom: 8px;
background: $menuBG;
border-radius: 3px;
font-size: 14px;
line-height: 24px;
max-height: 456px;
overflow: auto;
width: 300px;
&-ul {
margin:0;
padding:0;
@@ -16,37 +16,90 @@
}
}
&-header:hover {
background-color: initial;
cursor: initial;
&-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;
}
}
&-entry-text {
display: inline-block;
text-overflow: ellipsis;
max-width: 213px;
overflow: hidden;
white-space: nowrap;
&-entry {
align-items: center;
color: #fff;
cursor: pointer;
display: flex;
padding: 8px 0;
margin-left: 48px;
&.left-margin {
margin-left: 36px;
&--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;
}
}
&-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;
}
@@ -55,6 +108,19 @@
&-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;
@@ -74,21 +140,42 @@
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;
padding: 4px 10px;
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;
position: absolute;
right: 16px;
top: 6px;
top: 5px;
}
&-meter-mic {
@@ -97,7 +184,9 @@
top: 14px;
}
&-checkbox-container {
padding: 10px 16px;
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) {
outline: none;
padding: 0;
}
}

View File

@@ -3,28 +3,28 @@
display: inline-block;
& > svg {
fill: #525252;
fill: #4E5E6C;
width: 38px;
}
}
&.metr--disabled {
& > svg {
fill: #525252;
fill: #4E5E6C;
}
}
}
.metr-l-0 {
rect:first-child {
fill: #1EC26A;
fill: #31B76A;
}
}
@for $i from 1 through 7 {
.metr-l-#{$i} {
rect:nth-child(-n+#{$i+1}) {
fill: #1EC26A;
fill: #31B76A;
}
}
}

View File

@@ -469,7 +469,7 @@ PODS:
- React-Core
- RNDeviceInfo (8.4.8):
- React-Core
- RNGestureHandler (2.9.0):
- RNGestureHandler (2.8.0):
- React-Core
- RNGoogleSignin (7.0.4):
- GoogleSignIn (~> 6.0.0)
@@ -773,7 +773,7 @@ SPEC CHECKSUMS:
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 0400a6d0c94186d1120c3cbd97b23abc022187a9
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
RNGoogleSignin: c4381751eefd73c552b923ba347a9bfc6f18771c
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f

View File

@@ -40,10 +40,19 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
#pragma mark Initializers
- (instancetype)init {
self = [super init];
if (self) {
[self initWithXXX];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self doInitialize];
[self initWithXXX];
}
return self;
@@ -52,7 +61,7 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self doInitialize];
[self initWithXXX];
}
return self;
@@ -62,9 +71,9 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
* Internal initialization:
*
* - sets the background color
* - registers necessary observers
* - initializes the external API scope
*/
- (void)doInitialize {
- (void)initWithXXX {
// Set a background color which is in accord with the JavaScript and Android
// parts of the application and causes less perceived visual flicker than
// the default background color.

14
package-lock.json generated
View File

@@ -94,7 +94,7 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.9.0",
"react-native-gesture-handler": "2.8.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
@@ -16293,9 +16293,9 @@
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz",
"integrity": "sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz",
"integrity": "sha512-poOSfz/w0IyD6Qwq7aaIRRfEaVTl1ecQFoyiIbpOpfNTjm2B1niY2FLrdVQIOtIOe+K9nH55Qal04nr4jGkHdQ==",
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
@@ -32719,9 +32719,9 @@
"integrity": "sha512-MKbuBbovO8eGiAM9i6o0nrdBXivhRpzPQ+aVBXGJEPMH7RrCSNUKaCoEpkjfGHlTxjZimi6WjDCjjzCRSHlV1A=="
},
"react-native-gesture-handler": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz",
"integrity": "sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz",
"integrity": "sha512-poOSfz/w0IyD6Qwq7aaIRRfEaVTl1ecQFoyiIbpOpfNTjm2B1niY2FLrdVQIOtIOe+K9nH55Qal04nr4jGkHdQ==",
"requires": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",

View File

@@ -99,7 +99,7 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.9.0",
"react-native-gesture-handler": "2.8.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",

View File

@@ -21,7 +21,6 @@ import StartRecordingDialog from '../../recording/components/Recording/web/Start
import StopRecordingDialog from '../../recording/components/Recording/web/StopRecordingDialog';
// @ts-ignore
import RemoteControlAuthorizationDialog from '../../remote-control/components/RemoteControlAuthorizationDialog';
import PasswordRequiredPrompt from '../../room-lock/components/PasswordRequiredPrompt.web';
import SalesforceLinkDialog from '../../salesforce/components/web/SalesforceLinkDialog';
import ShareAudioDialog from '../../screen-share/components/web/ShareAudioDialog';
import ShareScreenWarningDialog from '../../screen-share/components/web/ShareScreenWarningDialog';
@@ -51,7 +50,7 @@ const NEW_DIALOG_LIST = [ KeyboardShortcutsDialog, ChatPrivacyDialog, DisplayNam
SharedVideoDialog, SpeakerStats, LanguageSelectorDialog, MuteEveryoneDialog, MuteEveryonesVideoDialog,
GrantModeratorDialog, KickRemoteParticipantDialog, MuteRemoteParticipantsVideoDialog, VideoQualityDialog,
VirtualBackgroundDialog, LoginDialog, WaitForOwnerDialog, DesktopPicker, RemoteControlAuthorizationDialog,
LogoutDialog, SalesforceLinkDialog, ParticipantVerificationDialog, PasswordRequiredPrompt ];
LogoutDialog, SalesforceLinkDialog, ParticipantVerificationDialog ];
// This function is necessary while the transition from @atlaskit dialog to our component is ongoing.
const isNewDialog = (component: any) => NEW_DIALOG_LIST.some(comp => comp === component);

View File

@@ -1,10 +1,10 @@
<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 x="5" 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="20" 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="35" width="3" height="12" rx="1" />
<svg width="38" height="12" viewBox="0 0 38 12" fill="#5E6D7A" xmlns="http://www.w3.org/2000/svg">
<rect 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="15" 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="30" width="3" height="12" rx="1" />
<rect x="35" width="3" height="12" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 487 B

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -52,7 +52,7 @@ const DEFAULT_STATE: ISettingsState = {
};
export interface ISettingsState {
audioOutputDeviceId?: string;
audioOutputDeviceId?: string | boolean;
audioSettingsVisible?: boolean;
avatarURL?: string;
cameraDeviceId?: string | boolean;
@@ -108,7 +108,6 @@ 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;

View File

@@ -34,8 +34,6 @@ const getComputedOuterHeight = (element: HTMLElement) => {
interface IProps {
[key: `aria-${string}`]: string;
/**
* Accessibility label for menu container.
*/
@@ -61,11 +59,6 @@ interface IProps {
*/
hidden?: boolean;
/**
* Optional id.
*/
id?: string;
/**
* Whether or not the menu is already in a drawer.
*/
@@ -105,11 +98,6 @@ 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;
@@ -158,7 +146,6 @@ const ContextMenu = ({
className,
entity,
hidden,
id,
inDrawer,
isDrawerOpen,
offsetTarget,
@@ -166,8 +153,7 @@ const ContextMenu = ({
onKeyDown,
onDrawerClose,
onMouseEnter,
onMouseLeave,
tabIndex
onMouseLeave
}: IProps) => {
const [ isHidden, setIsHidden ] = useState(true);
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -231,14 +217,11 @@ const ContextMenu = ({
isHidden && styles.contextMenuHidden,
className
) }
id = { id }
onClick = { onClick }
onKeyDown = { onKeyDown }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = 'menu'
tabIndex = { tabIndex }>
ref = { containerRef }>
{children}
</div>;
};

View File

@@ -13,11 +13,6 @@ export interface IProps {
*/
accessibilityLabel: string;
/**
* Component children.
*/
children?: ReactNode;
/**
* CSS class name used for custom styles.
*/
@@ -59,11 +54,6 @@ export interface IProps {
*/
onKeyPress?: (e?: React.KeyboardEvent) => void;
/**
* Whether the item is marked as selected.
*/
selected?: boolean;
/**
* TestId of the element, if any.
*/
@@ -72,7 +62,7 @@ export interface IProps {
/**
* Action text.
*/
text?: string;
text: string;
/**
* Class name for the text.
@@ -107,12 +97,6 @@ const useStyles = makeStyles()(theme => {
}
},
selected: {
borderLeft: `3px solid ${theme.palette.action01Hover}`,
paddingLeft: '13px',
backgroundColor: theme.palette.ui02
},
contextMenuItemDisabled: {
pointerEvents: 'none'
},
@@ -140,7 +124,6 @@ const useStyles = makeStyles()(theme => {
const ContextMenuItem = ({
accessibilityLabel,
children,
className,
customIcon,
disabled,
@@ -149,7 +132,6 @@ const ContextMenuItem = ({
onClick,
onKeyDown,
onKeyPress,
selected,
testId,
text,
textClassName }: IProps) => {
@@ -163,7 +145,6 @@ const ContextMenuItem = ({
className = { cx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,
disabled && styles.contextMenuItemDisabled,
selected && styles.selected,
className
) }
data-testid = { testId }
@@ -171,15 +152,13 @@ const ContextMenuItem = ({
key = { text }
onClick = { disabled ? undefined : onClick }
onKeyDown = { disabled ? undefined : onKeyDown }
onKeyPress = { disabled ? undefined : onKeyPress }
role = 'menuitem'>
onKeyPress = { disabled ? undefined : onKeyPress }>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }
size = { 20 }
src = { icon } />}
{text && <span className = { cx(styles.text, textClassName) }>{text}</span>}
{children}
<span className = { cx(styles.text, textClassName) }>{text}</span>
</div>
);
};

View File

@@ -124,6 +124,7 @@ const useStyles = (theme: Theme) => {
*/
class NotificationsContainer extends Component<IProps> {
_api: Object;
_timeouts: Map<string, number>;
/**
* Initializes a new {@code NotificationsContainer} instance.
@@ -133,6 +134,8 @@ class NotificationsContainer extends Component<IProps> {
constructor(props: IProps) {
super(props);
this._timeouts = new Map();
// Bind event handlers so they are only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
@@ -183,6 +186,13 @@ class NotificationsContainer extends Component<IProps> {
* @returns {void}
*/
_onDismissed(uid: string) {
const timeout = this._timeouts.get(`${uid}`);
if (timeout) {
clearTimeout(timeout);
this._timeouts.delete(`${uid}`);
}
this.props.dispatch(hideNotification(uid));
}

View File

@@ -1,12 +1,11 @@
// @flow
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import type { Dispatch } from 'redux';
import { IStore } from '../../app/types';
import { setPassword } from '../../base/conference/actions';
import { IJitsiConference } from '../../base/conference/reducer';
import { translate } from '../../base/i18n/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import { setPassword } from '../../base/conference';
import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import Input from '../../base/ui/components/web/Input';
import { _cancelPasswordRequiredPrompt } from '../actions';
@@ -14,18 +13,23 @@ import { _cancelPasswordRequiredPrompt } from '../actions';
* The type of the React {@code Component} props of
* {@link PasswordRequiredPrompt}.
*/
interface IProps extends WithTranslation {
type Props = {
/**
* The JitsiConference which requires a password.
*/
conference: IJitsiConference;
conference: Object,
/**
* The redux store's {@code dispatch} function.
*/
dispatch: IStore['dispatch'];
}
dispatch: Dispatch<any>,
/**
* The translate function.
*/
t: Function
};
/**
* The type of the React {@code Component} state of
@@ -36,14 +40,14 @@ type State = {
/**
* The password entered by the local participant.
*/
password?: string;
};
password: string
}
/**
* Implements a React Component which prompts the user when a password is
* required to join a conference.
*/
class PasswordRequiredPrompt extends Component<IProps, State> {
class PasswordRequiredPrompt extends Component<Props, State> {
state = {
password: ''
};
@@ -54,7 +58,7 @@ class PasswordRequiredPrompt extends Component<IProps, State> {
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
@@ -72,10 +76,12 @@ class PasswordRequiredPrompt extends Component<IProps, State> {
render() {
return (
<Dialog
disableBackdropClose = { true }
disableBlanketClickDismiss = { true }
isModal = { false }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = 'dialog.passwordRequired'>
titleKey = 'dialog.passwordRequired'
width = 'small'>
{ this._renderBody() }
</Dialog>
);
@@ -92,7 +98,6 @@ class PasswordRequiredPrompt extends Component<IProps, State> {
<div>
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
label = { this.props.t('dialog.passwordLabel') }
name = 'lockKey'
onChange = { this._onPasswordChanged }
@@ -102,6 +107,8 @@ class PasswordRequiredPrompt extends Component<IProps, State> {
);
}
_onPasswordChanged: ({ target: { value: * }}) => void;
/**
* Notifies this dialog that password has changed.
*
@@ -115,6 +122,8 @@ class PasswordRequiredPrompt extends Component<IProps, State> {
});
}
_onCancel: () => boolean;
/**
* Dispatches action to cancel and dismiss this dialog.
*
@@ -129,6 +138,8 @@ class PasswordRequiredPrompt extends Component<IProps, State> {
return true;
}
_onSubmit: () => boolean;
/**
* Dispatches action to submit value from this dialog.
*

View File

@@ -1,21 +1,14 @@
// @flow
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../../app/types';
import { translate } from '../../../../base/i18n/functions';
import { IconMic, IconVolumeUp } from '../../../../base/icons/svg';
import { translate } from '../../../../base/i18n';
import { IconMic, IconVolumeUp } from '../../../../base/icons';
import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
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 { equals } from '../../../../base/redux';
import { createLocalAudioTracks } from '../../../functions';
import AudioSettingsHeader from './AudioSettingsHeader';
import MicrophoneEntry from './MicrophoneEntry';
import SpeakerEntry from './SpeakerEntry';
@@ -29,75 +22,65 @@ const browser = JitsiMeetJS.util.browser;
* @param {Function} t - The translation function.
* @returns {string}
*/
function transformDefaultDeviceLabel(deviceId: string, label: string, t: Function) {
function transformDefaultDeviceLabel(deviceId, label, t) {
return deviceId === 'default'
? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
: label;
}
export interface IProps extends WithTranslation {
export type Props = {
/**
/**
* 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,
/**
* A list with objects containing the labels and deviceIds
* of all the input devices.
/**
* Used to set a new microphone as the current one.
*/
microphoneDevices: Array<{ deviceId: string; label: string; }>;
setAudioInputDevice: Function,
/**
* Whether noise suppression is enabled or not.
*/
noiseSuppressionEnabled: boolean;
/**
* 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: Array<{ deviceId: string; label: string; }>;
outputDevices: Object[],
/**
* Whether the prejoin page is visible or not.
*/
prejoinVisible: boolean;
/**
* Used to set a new microphone as the current one.
/**
* A list with objects containing the labels and deviceIds
* of all the input devices.
*/
setAudioInputDevice: Function;
microphoneDevices: Object[],
/**
* Used to set a new output device as the current one.
*/
setAudioOutputDevice: Function;
/**
* Function to toggle noise suppression.
* Invoked to obtain translated strings.
*/
toggleSuppression: () => void;
}
t: Function
};
type State = {
/**
/**
* An list of objects, each containing the microphone label, audio track, device id
* and track error if the case.
*/
audioTracks: Array<{ deviceId: string; hasError: boolean; jitsiTrack: any; label: string; }>;
};
audioTracks: Object[]
}
/**
* Implements a React {@link Component} which displays a list of all
@@ -105,8 +88,9 @@ type State = {
*
* @augments Component
*/
class AudioSettingsContent extends Component<IProps, State> {
class AudioSettingsContent extends Component<Props, State> {
_componentWasUnmounted: boolean;
_audioContentRef: Object;
microphoneHeaderId = 'microphone_settings_header';
speakerHeaderId = 'speaker_settings_header';
@@ -117,11 +101,13 @@ class AudioSettingsContent extends Component<IProps, State> {
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
constructor(props) {
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 }) => {
@@ -134,6 +120,23 @@ class AudioSettingsContent extends Component<IProps, 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.
@@ -141,17 +144,19 @@ class AudioSettingsContent extends Component<IProps, State> {
* @param {string} deviceId - The deviceId for the clicked microphone.
* @returns {void}
*/
_onMicrophoneEntryClick(deviceId: string) {
_onMicrophoneEntryClick(deviceId) {
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: string) {
_onSpeakerEntryClick(deviceId) {
this.props.setAudioOutputDevice(deviceId);
}
@@ -164,8 +169,7 @@ class AudioSettingsContent extends Component<IProps, State> {
* @param {Function} t - The translation function.
* @returns {React$Node}
*/
_renderMicrophoneEntry(data: { deviceId: string; hasError: boolean; jitsiTrack: any; label: string; },
index: number, length: number, t: Function) {
_renderMicrophoneEntry(data, index, length, t) {
const { deviceId, jitsiTrack, hasError } = data;
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
const isSelected = deviceId === this.props.currentMicDeviceId;
@@ -196,7 +200,7 @@ class AudioSettingsContent extends Component<IProps, State> {
* @param {Function} t - The translation function.
* @returns {React$Node}
*/
_renderSpeakerEntry(data: { deviceId: string; label: string; }, index: number, length: number, t: Function) {
_renderSpeakerEntry(data, index, length, t) {
const { deviceId } = data;
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
const key = `se-${index}`;
@@ -249,9 +253,9 @@ class AudioSettingsContent extends Component<IProps, State> {
* @param {Object} audioTracks - The object holding the audio tracks.
* @returns {void}
*/
_disposeTracks(audioTracks: Array<{ jitsiTrack: any; }>) {
_disposeTracks(audioTracks) {
audioTracks.forEach(({ jitsiTrack }) => {
jitsiTrack?.dispose();
jitsiTrack && jitsiTrack.dispose();
});
}
@@ -279,7 +283,7 @@ class AudioSettingsContent extends Component<IProps, State> {
*
* @inheritdoc
*/
componentDidUpdate(prevProps: IProps) {
componentDidUpdate(prevProps) {
if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) {
this._setTracks();
}
@@ -292,82 +296,55 @@ class AudioSettingsContent extends Component<IProps, State> {
* @inheritdoc
*/
render() {
const { outputDevices, t, noiseSuppressionEnabled, toggleSuppression, prejoinVisible } = this.props;
const { outputDevices, t } = this.props;
return (
<ContextMenu
aria-labelledby = 'audio-settings-button'
className = 'audio-preview-content'
hidden = { false }
id = 'audio-settings-dialog'
tabIndex = { -1 }>
<ContextMenuItemGroup>
<ContextMenuItem
accessibilityLabel = { t('settings.microphones') }
className = 'audio-preview-header'
icon = { IconMic }
id = { this.microphoneHeaderId }
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') } />
<div>
<div
aria-labelledby = 'audio-settings-button'
className = 'audio-preview-content'
id = 'audio-settings-dialog'
onKeyDown = { this._onEscClick }
ref = { this._audioContentRef }
role = 'menu'
tabIndex = { -1 }>
<div role = 'menuitem'>
<AudioSettingsHeader
IconComponent = { IconMic }
id = { this.microphoneHeaderId }
text = { t('settings.microphones') } />
<ul
aria-labelledby = { this.speakerHeaderId }
aria-labelledby = 'microphone_settings_header'
className = 'audio-preview-content-ul'
role = 'radiogroup'
tabIndex = { -1 }>
{ outputDevices.map((data, i) =>
this._renderSpeakerEntry(data, i, outputDevices.length, t)
tabIndex = '-1'>
{this.state.audioTracks.map((data, i) =>
this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t)
)}
</ul>
</ContextMenuItemGroup>)
}
{!prejoinVisible && (
<ContextMenuItemGroup>
<div
className = 'audio-preview-checkbox-container'
// eslint-disable-next-line react/jsx-no-bind
onClick = { e => e.stopPropagation() }>
<Checkbox
checked = { noiseSuppressionEnabled }
label = { t('toolbar.noiseSuppression') }
onChange = { toggleSuppression } />
</div>
</ContextMenuItemGroup>
)}
</ContextMenu>
</div>
{ outputDevices.length > 0 && (
<div role = 'menuitem'>
<hr className = 'audio-preview-hr' />
<AudioSettingsHeader
IconComponent = { IconVolumeUp }
id = { this.speakerHeaderId }
text = { t('settings.speakers') } />
<ul
aria-labelledby = 'speaker_settings_header'
className = 'audio-preview-content-ul'
role = 'radiogroup'
tabIndex = '-1'>
{ outputDevices.map((data, i) =>
this._renderSpeakerEntry(data, i, outputDevices.length, t)
)}
</ul>
</div>)
}
</div>
</div>
);
}
}
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));
export default translate(AudioSettingsContent);

View File

@@ -0,0 +1,64 @@
// @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>
);
}

View File

@@ -0,0 +1,48 @@
// @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>
);
}

View File

@@ -1,8 +1,8 @@
import React, { ReactNode } from 'react';
import { connect } from 'react-redux';
// @flow
import { IReduxState } from '../../../../app/types';
import { areAudioLevelsEnabled } from '../../../../base/config/functions.web';
import React from 'react';
import { areAudioLevelsEnabled } from '../../../../base/config/functions';
import {
setAudioInputDeviceAndUpdateSettings,
setAudioOutputDevice as setAudioOutputDeviceAction
@@ -12,38 +12,39 @@ 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/functions.web';
} from '../../../../base/settings';
import { toggleAudioSettings } from '../../../actions';
import { getAudioSettingsVisibility } from '../../../functions.web';
import { getAudioSettingsVisibility } from '../../../functions';
import AudioSettingsContent, { type IProps as AudioSettingsContentProps } from './AudioSettingsContent';
import AudioSettingsContent, { type Props as AudioSettingsContentProps } from './AudioSettingsContent';
interface IProps extends AudioSettingsContentProps {
type Props = AudioSettingsContentProps & {
/**
* Component's children (the audio button).
*/
children: ReactNode;
children: React$Node,
/**
* 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
}
/**
@@ -63,7 +64,7 @@ function AudioSettingsPopup({
outputDevices,
popupPlacement,
measureAudioLevels
}: IProps) {
}: Props) {
return (
<div className = 'audio-preview'>
<Popover
@@ -91,16 +92,16 @@ function AudioSettingsPopup({
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
function mapStateToProps(state) {
const { clientWidth } = state['features/base/responsive-ui'];
return {
popupPlacement: clientWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
popupPlacement: clientWidth <= SMALL_MOBILE_WIDTH ? 'auto' : 'top-end',
currentMicDeviceId: getCurrentMicDeviceId(state),
currentOutputDeviceId: getCurrentOutputDeviceId(state),
isOpen: Boolean(getAudioSettingsVisibility(state)),
microphoneDevices: getAudioInputDeviceData(state) ?? [],
outputDevices: getAudioOutputDeviceData(state) ?? [],
isOpen: getAudioSettingsVisibility(state),
microphoneDevices: getAudioInputDeviceData(state),
outputDevices: getAudioOutputDeviceData(state),
measureAudioLevels: areAudioLevelsEnabled(state)
};
}

View File

@@ -1,33 +1,34 @@
// @flow
import React from 'react';
import Icon from '../../../../base/icons/components/Icon';
import { IconMeter } from '../../../../base/icons/svg';
import { Icon, IconMeter } from '../../../../base/icons';
interface IProps {
type Props = {
/**
* Own class name for the component.
*/
className: string;
className: string,
/**
* Flag indicating whether the component is greyed out/disabled.
*/
isDisabled?: boolean;
isDisabled?: boolean,
/**
* The level of the meter.
* Should be between 0 and 7 as per the used SVG.
*/
level: number;
}
level: number,
};
/**
* React {@code Component} representing an audio level meter.
*
* @returns { ReactElement}
*/
export default function({ className, isDisabled, level }: IProps) {
export default function({ className, isDisabled, level }: Props) {
let ownClassName;
if (level > -1) {

View File

@@ -1,80 +1,61 @@
import clsx from 'clsx';
// @flow
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 = {
/**
* The text for this component.
*/
children: string;
type Props = AudioSettingsEntryProps & {
/**
* 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;
/**
* Flag indicating the selection state.
*/
isSelected: boolean;
index?: number,
/**
* The audio track for the current entry.
*/
jitsiTrack: any;
/**
* The id for the label, that contains the item text.
*/
labelId?: string;
jitsiTrack: Object,
/**
* The length of the microphone list.
*/
length: number;
length: number,
listHeaderId: string;
/**
* Used to decide whether to listen to audio level changes.
*/
measureAudioLevels: boolean;
/**
* Click handler for component.
*/
onClick: Function;
};
onClick: Function,
listHeaderId: string,
/**
* Used to decide whether to listen to audio level changes.
*/
measureAudioLevels: boolean,
}
type State = {
/**
* The audio level.
*/
level: number;
};
level: number
}
/**
* React {@code Component} representing an entry for the microphone audio settings.
@@ -100,6 +81,8 @@ export default class MicrophoneEntry extends Component<Props, State> {
this._updateLevel = this._updateLevel.bind(this);
}
_onClick: () => void;
/**
* Click handler for the entry.
*
@@ -109,6 +92,13 @@ export default class MicrophoneEntry extends Component<Props, State> {
this.props.onClick(this.props.deviceId);
}
/**
* Key pressed handler for the entry.
*
* @returns {void}
*/
_onKeyPress: (KeyboardEvent) => void;
/**
* Key pressed handler for the entry.
*
@@ -117,20 +107,22 @@ export default class MicrophoneEntry extends Component<Props, State> {
*
* @returns {void}
*/
_onKeyPress(e: React.KeyboardEvent) {
_onKeyPress(e) {
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: number) {
_updateLevel(num) {
this.setState({
level: Math.floor(num / 0.125)
});
@@ -155,8 +147,8 @@ export default class MicrophoneEntry extends Component<Props, State> {
* @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from.
* @returns {void}
*/
_stopListening(jitsiTrack?: any) {
jitsiTrack?.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel);
_stopListening(jitsiTrack) {
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel);
this.setState({
level: -1
});
@@ -210,9 +202,9 @@ export default class MicrophoneEntry extends Component<Props, State> {
measureAudioLevels
} = this.props;
const deviceTextId = `choose_microphone${deviceId}`;
const deviceTextId: string = `choose_microphone${deviceId}`;
const labelledby = `${listHeaderId} ${deviceTextId} `;
const labelledby: string = `${listHeaderId} ${deviceTextId} `;
const className = `audio-preview-microphone ${measureAudioLevels
? 'audio-preview-microphone--withmeter' : 'audio-preview-microphone--nometer'}`;
@@ -228,17 +220,12 @@ export default class MicrophoneEntry extends Component<Props, State> {
onKeyPress = { this._onKeyPress }
role = 'radio'
tabIndex = { 0 }>
<ContextMenuItem
accessibilityLabel = ''
icon = { isSelected ? IconCheck : undefined }
selected = { isSelected }
text = { children }
textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }>
{hasError && <Icon
className = 'audio-preview-icon audio-preview-icon--exclamation'
size = { 16 }
src = { IconExclamationSolid } />}
</ContextMenuItem>
<AudioSettingsEntry
hasError = { hasError }
isSelected = { isSelected }
labelId = { deviceTextId }>
{children}
</AudioSettingsEntry>
{ Boolean(jitsiTrack) && measureAudioLevels && <Meter
className = 'audio-preview-meter-mic'
isDisabled = { hasError }

View File

@@ -0,0 +1,163 @@
// @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>
);
}
}

View File

@@ -1,140 +0,0 @@
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;

View File

@@ -0,0 +1,34 @@
// @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>
);
}

View File

@@ -42,7 +42,7 @@ export function createLocalVideoTracks(ids: string[], timeout?: number) {
* label: string
* }[]>}
*/
export function createLocalAudioTracks(devices: Array<{ deviceId: string; label: string; }>, timeout?: number) {
export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) {
return Promise.all(
devices.map(async ({ deviceId, label }) => {
let jitsiTrack = null;

View File

@@ -1,8 +1,5 @@
import { ReactNode, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
type Props = {
@@ -39,7 +36,6 @@ 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');
@@ -96,7 +92,7 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) {
document.body.removeChild(portalTarget);
}
};
}, [ clientWidth ]);
}, []);
return ReactDOM.createPortal(
children,