mirror of https://github.com/jitsi/jitsi-meet
parent
17ca9722b7
commit
08f55ccb94
@ -0,0 +1,124 @@ |
||||
.audio-preview { |
||||
&-content { |
||||
font-size: 15px; |
||||
line-height: 24px; |
||||
max-height: 456px; |
||||
overflow: auto; |
||||
width: 328px; |
||||
} |
||||
|
||||
&-header { |
||||
color: #fff; |
||||
display: flex; |
||||
padding: 16px; |
||||
|
||||
&-icon { |
||||
display: inline-block; |
||||
} |
||||
|
||||
&-text { |
||||
font-weight: bold; |
||||
margin-left: 8px; |
||||
} |
||||
} |
||||
|
||||
&-entry { |
||||
align-items: center; |
||||
color: #fff; |
||||
cursor: pointer; |
||||
display: flex; |
||||
padding: 12px 0; |
||||
margin-left: 48px; |
||||
|
||||
&--selected { |
||||
background: rgba(28,32,37,0.5); |
||||
cursor: initial; |
||||
margin-left: 0; |
||||
padding-left: 21px; |
||||
} |
||||
|
||||
&-text { |
||||
color: #fff; |
||||
font-size: 15px; |
||||
display: inline-block; |
||||
line-height: 24px; |
||||
text-overflow: ellipsis; |
||||
max-width: 213px; |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
} |
||||
} |
||||
|
||||
&-speaker { |
||||
position: relative; |
||||
|
||||
&:hover { |
||||
.audio-preview-entry { |
||||
background: rgba(255,255,255, 0.2); |
||||
margin-left: 0; |
||||
padding-left: 48px; |
||||
|
||||
&--selected { |
||||
padding-left: 21px; |
||||
} |
||||
} |
||||
|
||||
.audio-preview-test-button { |
||||
display: inline-block; |
||||
} |
||||
} |
||||
|
||||
.audio-preview-entry-text { |
||||
max-width: 256px; |
||||
} |
||||
} |
||||
|
||||
&-microphone { |
||||
position: relative; |
||||
} |
||||
|
||||
|
||||
&-icon { |
||||
border-radius: 50%; |
||||
display: inline-block; |
||||
width: 14px; |
||||
|
||||
& svg { |
||||
fill: #1C2025; |
||||
} |
||||
|
||||
&--check { |
||||
background: #31B76A; |
||||
margin-right: 13px; |
||||
} |
||||
|
||||
&--exclamation { |
||||
margin-left: 6px; |
||||
& svg { |
||||
fill: #E54B4B; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&-test-button { |
||||
display: none; |
||||
background: #FFF; |
||||
border: 1px solid #D1DBE8; |
||||
border-radius: 3px; |
||||
color: #1C2025; |
||||
cursor: pointer; |
||||
font-weight: 600; |
||||
font-size: 15px; |
||||
line-height: 24px; |
||||
padding: 4px 16px; |
||||
position: absolute; |
||||
right: 16px; |
||||
top: 8px; |
||||
} |
||||
|
||||
&-meter-mic { |
||||
position: absolute; |
||||
right: 16px; |
||||
top: 18px; |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
.jitsi-icon { |
||||
&.metr { |
||||
display: inline-block; |
||||
|
||||
& > svg { |
||||
fill: #76CF9C; |
||||
width: 38px; |
||||
} |
||||
} |
||||
|
||||
&.metr--disabled { |
||||
& > svg { |
||||
fill: #5E6D7A; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.metr-l-0 { |
||||
rect:first-child { |
||||
fill: #279255; |
||||
} |
||||
} |
||||
|
||||
@for $i from 1 through 7 { |
||||
.metr-l-#{$i} { |
||||
rect:nth-child(-n+#{$i+1}) { |
||||
fill: #31B76A; |
||||
} |
||||
rect:first-child { |
||||
fill: #279255; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,76 @@ |
||||
.settings-button { |
||||
&-container { |
||||
position: relative; |
||||
|
||||
.toolbox-icon { |
||||
align-items: center; |
||||
cursor: pointer; |
||||
display: flex; |
||||
background-color: #fff; |
||||
border-radius: 50%; |
||||
border: 1px solid #d1dbe8; |
||||
justify-content: center; |
||||
width: 38px; |
||||
height: 38px; |
||||
|
||||
&:hover { |
||||
background-color: #daebfa; |
||||
border: 1px solid #daebfa; |
||||
} |
||||
|
||||
&.toggled { |
||||
background: #2a3a4b; |
||||
border: 1px solid #5e6d7a; |
||||
|
||||
svg { |
||||
fill: #fff; |
||||
} |
||||
|
||||
&:hover { |
||||
background-color: #5e6d7a; |
||||
} |
||||
} |
||||
|
||||
&.disabled, .disabled & { |
||||
cursor: initial; |
||||
color: #fff; |
||||
background-color: #a4b8d1; |
||||
} |
||||
|
||||
svg { |
||||
fill: #5e6d7a; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&-small-icon { |
||||
background: #FFF; |
||||
border: 1px solid rgba(0, 0, 0, 0.2); |
||||
border-radius: 50%; |
||||
bottom: 0; |
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.25); |
||||
cursor: pointer; |
||||
height: 18px; |
||||
position: absolute; |
||||
text-align: center; |
||||
right: 2px; |
||||
width: 18px; |
||||
|
||||
&:hover { |
||||
background-color: #daebfa; |
||||
} |
||||
|
||||
&> svg { |
||||
margin-top: 5px; |
||||
} |
||||
|
||||
&--disabled { |
||||
background-color: #a4b8d1; |
||||
cursor: default; |
||||
|
||||
&:hover { |
||||
background-color: #a4b8d1; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
.video-preview { |
||||
&-entry { |
||||
cursor: pointer; |
||||
height: 135px; |
||||
margin-bottom: 16px; |
||||
position: relative; |
||||
width: 240px; |
||||
|
||||
&:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
&--selected { |
||||
border: 3px solid #31B76A; |
||||
cursor: default; |
||||
height: 129px; |
||||
width: 234px; |
||||
} |
||||
} |
||||
|
||||
&-video { |
||||
height: 100%; |
||||
object-fit: cover; |
||||
width: 100%; |
||||
} |
||||
|
||||
&-overlay { |
||||
background: rgba(42, 58, 75, 0.6); |
||||
height: 100%; |
||||
position: absolute; |
||||
width: 100%; |
||||
z-index: 1; |
||||
} |
||||
|
||||
&-error { |
||||
align-items: center; |
||||
display: flex; |
||||
height: 100%; |
||||
justify-content: center; |
||||
position: absolute; |
||||
width: 100%; |
||||
} |
||||
} |
After Width: | Height: | Size: 509 B |
After Width: | Height: | Size: 731 B |
After Width: | Height: | Size: 1001 B |
After Width: | Height: | Size: 457 B |
After Width: | Height: | Size: 817 B |
After Width: | Height: | Size: 733 B |
@ -0,0 +1,66 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { Icon } from '../../icons'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The decorated component (ToolboxButton). |
||||
*/ |
||||
children: React$Node, |
||||
|
||||
/** |
||||
* Icon of the button. |
||||
*/ |
||||
icon: Function, |
||||
|
||||
/** |
||||
* Flag used for disabling the small icon. |
||||
*/ |
||||
iconDisabled: boolean, |
||||
|
||||
/** |
||||
* Click handler for the small icon. |
||||
*/ |
||||
onIconClick: Function, |
||||
|
||||
/** |
||||
* Additional styles. |
||||
*/ |
||||
styles?: Object, |
||||
} |
||||
|
||||
/** |
||||
* Displayes the `ToolboxButtonWithIcon` component. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
export default function ToolboxButtonWithIcon({ |
||||
children, |
||||
icon, |
||||
iconDisabled, |
||||
onIconClick, |
||||
styles |
||||
}: Props) { |
||||
const iconProps = {}; |
||||
|
||||
if (iconDisabled) { |
||||
iconProps.className = 'settings-button-small-icon settings-button-small-icon--disabled'; |
||||
} else { |
||||
iconProps.className = 'settings-button-small-icon'; |
||||
iconProps.onClick = onIconClick; |
||||
} |
||||
|
||||
return ( |
||||
<div |
||||
className = 'settings-button-container' |
||||
styles = { styles }> |
||||
{ children } |
||||
<Icon |
||||
{ ...iconProps } |
||||
size = { 9 } |
||||
src = { icon } /> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,262 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import AudioSettingsHeader from './AudioSettingsHeader'; |
||||
import { translate } from '../../../../base/i18n'; |
||||
import { IconMicrophoneEmpty, IconVolumeEmpty } from '../../../../base/icons'; |
||||
import { createLocalAudioTrack } from '../../../functions'; |
||||
import MicrophoneEntry from './MicrophoneEntry'; |
||||
import SpeakerEntry from './SpeakerEntry'; |
||||
|
||||
export type Props = { |
||||
|
||||
/** |
||||
* The deviceId of the microphone in use. |
||||
*/ |
||||
currentMicDeviceId: string, |
||||
|
||||
/** |
||||
* The deviceId of the output device in use. |
||||
*/ |
||||
currentOutputDeviceId: string, |
||||
|
||||
/** |
||||
* Used to set a new microphone as the current one. |
||||
*/ |
||||
setAudioInputDevice: Function, |
||||
|
||||
/** |
||||
* Used to set a new output device as the current one. |
||||
*/ |
||||
setAudioOutputDevice: Function, |
||||
|
||||
/** |
||||
* A list of objects containing the labels and deviceIds |
||||
* of all the output devices. |
||||
*/ |
||||
outputDevices: Object[], |
||||
|
||||
/** |
||||
* A list with objects containing the labels and deviceIds |
||||
* of all the input devices. |
||||
*/ |
||||
microphoneDevices: Object[], |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
type State = { |
||||
|
||||
/** |
||||
* An object containing the jitsiTrack and the error (if the case) |
||||
* for the microphone that is in use. |
||||
*/ |
||||
currentMicData: Object |
||||
} |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displayes a list of all |
||||
* the audio input & output devices to choose from. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class AudioSettingsContent extends Component<Props, State> { |
||||
_componentWasUnmounted: boolean; |
||||
|
||||
/** |
||||
* Initializes a new {@code AudioSettingsContent} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this); |
||||
this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this); |
||||
|
||||
this.state = { |
||||
currentMicData: { |
||||
error: false, |
||||
jitsiTrack: null |
||||
} |
||||
}; |
||||
} |
||||
|
||||
_onMicrophoneEntryClick: (string) => void; |
||||
|
||||
/** |
||||
* Click handler for the microphone entries. |
||||
* |
||||
* @param {string} deviceId - The deviceId for the clicked microphone. |
||||
* @returns {void} |
||||
*/ |
||||
_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) { |
||||
this.props.setAudioOutputDevice(deviceId); |
||||
} |
||||
|
||||
/** |
||||
* Renders a single microphone entry. |
||||
* |
||||
* @param {Object} data - An object with the deviceId and label of the microphone. |
||||
* @param {number} index - The index of the element, used for creating a key. |
||||
* @returns {React$Node} |
||||
*/ |
||||
_renderMicrophoneEntry(data, index) { |
||||
const { deviceId, label } = data; |
||||
const key = `me-${index}`; |
||||
const isSelected = deviceId === this.props.currentMicDeviceId; |
||||
let jitsiTrack = null; |
||||
let hasError = false; |
||||
|
||||
if (isSelected) { |
||||
({ jitsiTrack, hasError } = this.state.currentMicData); |
||||
} |
||||
|
||||
return ( |
||||
<MicrophoneEntry |
||||
deviceId = { deviceId } |
||||
hasError = { hasError } |
||||
isSelected = { isSelected } |
||||
jitsiTrack = { jitsiTrack } |
||||
key = { key } |
||||
onClick = { this._onMicrophoneEntryClick }> |
||||
{label} |
||||
</MicrophoneEntry> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders a single speaker entry. |
||||
* |
||||
* @param {Object} data - An object with the deviceId and label of the speaker. |
||||
* @param {number} index - The index of the element, used for creating a key. |
||||
* @returns {React$Node} |
||||
*/ |
||||
_renderSpeakerEntry(data, index) { |
||||
const { deviceId, label } = data; |
||||
const key = `se-${index}`; |
||||
|
||||
return ( |
||||
<SpeakerEntry |
||||
deviceId = { deviceId } |
||||
isSelected = { deviceId === this.props.currentOutputDeviceId } |
||||
key = { key } |
||||
onClick = { this._onSpeakerEntryClick }> |
||||
{label} |
||||
</SpeakerEntry> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Disposes the audio track for a given micData object. |
||||
* |
||||
* @param {Object} micData - The object holding the track. |
||||
* @returns {Promise<void>} |
||||
*/ |
||||
_disposeTrack(micData) { |
||||
const { jitsiTrack } = micData; |
||||
|
||||
return jitsiTrack ? jitsiTrack.dispose() : Promise.resolve(); |
||||
} |
||||
|
||||
/** |
||||
* Updates the current microphone data. |
||||
* Disposes previously created track and creates a new one. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
async _updateCurrentMicData() { |
||||
await this._disposeTrack(this.state.currentMicData); |
||||
|
||||
const currentMicData = await createLocalAudioTrack( |
||||
this.props.currentMicDeviceId, |
||||
); |
||||
|
||||
// In case the component gets unmounted before the track is created
|
||||
// avoid a leak by not setting the state
|
||||
if (this._componentWasUnmounted) { |
||||
this._disposeTrack(currentMicData); |
||||
} else { |
||||
this.setState({ |
||||
currentMicData |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidUpdate}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidUpdate(prevProps) { |
||||
if (prevProps.currentMicDeviceId !== this.props.currentMicDeviceId) { |
||||
this._updateCurrentMicData(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._updateCurrentMicData(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentWillUnmount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
this._componentWasUnmounted = true; |
||||
this._disposeTrack(this.state.currentMicData); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { microphoneDevices, outputDevices, t } = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
<div className = 'audio-preview-content'> |
||||
<AudioSettingsHeader |
||||
IconComponent = { IconMicrophoneEmpty } |
||||
text = { t('settings.selectMic') } /> |
||||
{microphoneDevices.map((data, i) => |
||||
this._renderMicrophoneEntry(data, i), |
||||
)} |
||||
<AudioSettingsHeader |
||||
IconComponent = { IconVolumeEmpty } |
||||
text = { t('settings.speakers') } /> |
||||
{outputDevices.map((data, i) => |
||||
this._renderSpeakerEntry(data, i), |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(AudioSettingsContent); |
@ -0,0 +1,53 @@ |
||||
// @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, |
||||
|
||||
/** |
||||
* 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, 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'>{children}</span> |
||||
{hasError && <Icon |
||||
className = 'audio-preview-icon audio-preview-icon--exclamation' |
||||
size = { 16 } |
||||
src = { IconExclamationSolid } />} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,39 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { Icon } from '../../../../base/icons'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link AudioSettingsHeader}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* 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, text }: Props) { |
||||
return ( |
||||
<div className = 'audio-preview-header'> |
||||
<div className = 'audio-preview-header-icon'> |
||||
{ <Icon |
||||
color = '#A4B8D1' |
||||
size = { 24 } |
||||
src = { IconComponent } />} |
||||
</div> |
||||
<div className = 'audio-preview-header-text'>{text}</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,97 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import InlineDialog from '@atlaskit/inline-dialog'; |
||||
|
||||
import AudioSettingsContent, { type Props as AudioSettingsContentProps } from './AudioSettingsContent'; |
||||
import { toggleAudioSettings } from '../../../actions'; |
||||
import { |
||||
getAudioInputDeviceData, |
||||
getAudioOutputDeviceData, |
||||
setAudioInputDevice as setAudioInputDeviceAction, |
||||
setAudioOutputDevice as setAudioOutputDeviceAction |
||||
} from '../../../../base/devices'; |
||||
import { connect } from '../../../../base/redux'; |
||||
import { getAudioSettingsVisibility } from '../../../functions'; |
||||
import { |
||||
getCurrentMicDeviceId, |
||||
getCurrentOutputDeviceId |
||||
} from '../../../../base/settings'; |
||||
|
||||
|
||||
type Props = AudioSettingsContentProps & { |
||||
|
||||
/** |
||||
* Component's children (the audio button). |
||||
*/ |
||||
children: React$Node, |
||||
|
||||
/** |
||||
* Flag controlling the visibility of the popup. |
||||
*/ |
||||
isOpen: boolean, |
||||
|
||||
/** |
||||
* Callback executed when the popup closes. |
||||
*/ |
||||
onClose: Function, |
||||
} |
||||
|
||||
/** |
||||
* Popup with audio settings. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
function AudioSettingsPopup({ |
||||
children, |
||||
currentMicDeviceId, |
||||
currentOutputDeviceId, |
||||
isOpen, |
||||
microphoneDevices, |
||||
setAudioInputDevice, |
||||
setAudioOutputDevice, |
||||
onClose, |
||||
outputDevices |
||||
}: Props) { |
||||
return ( |
||||
<div className = 'audio-preview'> |
||||
<InlineDialog |
||||
content = { <AudioSettingsContent |
||||
currentMicDeviceId = { currentMicDeviceId } |
||||
currentOutputDeviceId = { currentOutputDeviceId } |
||||
microphoneDevices = { microphoneDevices } |
||||
outputDevices = { outputDevices } |
||||
setAudioInputDevice = { setAudioInputDevice } |
||||
setAudioOutputDevice = { setAudioOutputDevice } /> } |
||||
isOpen = { isOpen } |
||||
onClose = { onClose } |
||||
position = 'top left'> |
||||
{children} |
||||
</InlineDialog> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Function that maps parts of Redux state tree into component props. |
||||
* |
||||
* @param {Object} state - Redux state. |
||||
* @returns {Object} |
||||
*/ |
||||
function mapStateToProps(state) { |
||||
return { |
||||
currentMicDeviceId: getCurrentMicDeviceId(state), |
||||
currentOutputDeviceId: getCurrentOutputDeviceId(state), |
||||
isOpen: getAudioSettingsVisibility(state), |
||||
microphoneDevices: getAudioInputDeviceData(state), |
||||
outputDevices: getAudioOutputDeviceData(state) |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
onClose: toggleAudioSettings, |
||||
setAudioInputDevice: setAudioInputDeviceAction, |
||||
setAudioOutputDevice: setAudioOutputDeviceAction |
||||
}; |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AudioSettingsPopup); |
@ -0,0 +1,45 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { Icon, IconMeter } from '../../../../base/icons'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Own class name for the component. |
||||
*/ |
||||
className: string, |
||||
|
||||
/** |
||||
* Flag indicating whether the component is greyed out/disabled. |
||||
*/ |
||||
isDisabled?: boolean, |
||||
|
||||
/** |
||||
* The level of the meter. |
||||
* Should be between 0 and 7 as per the used SVG. |
||||
*/ |
||||
level: number, |
||||
}; |
||||
|
||||
/** |
||||
* React {@code Component} representing an audio level meter. |
||||
* |
||||
* @returns { ReactElement} |
||||
*/ |
||||
export default function({ className, isDisabled, level }: Props) { |
||||
let ownClassName; |
||||
|
||||
if (level > -1) { |
||||
ownClassName = `metr metr-l-${level}`; |
||||
} else { |
||||
ownClassName = `metr ${isDisabled ? 'metr--disabled' : ''}`; |
||||
} |
||||
|
||||
return ( |
||||
<Icon |
||||
className = { `${ownClassName} ${className}` } |
||||
size = { 12 } |
||||
src = { IconMeter } /> |
||||
); |
||||
} |
@ -0,0 +1,172 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import AudioSettingsEntry, { type Props as AudioSettingsEntryProps } from './AudioSettingsEntry'; |
||||
import JitsiMeetJS from '../../../../base/lib-jitsi-meet/_'; |
||||
import Meter from './Meter'; |
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track; |
||||
|
||||
type Props = AudioSettingsEntryProps & { |
||||
|
||||
/** |
||||
* The deviceId of the microphone. |
||||
*/ |
||||
deviceId: string, |
||||
|
||||
/** |
||||
* Flag indicating if there is a problem with the device. |
||||
*/ |
||||
hasError?: boolean, |
||||
|
||||
/** |
||||
* The audio track for the current entry. |
||||
*/ |
||||
jitsiTrack: Object, |
||||
|
||||
/** |
||||
* Click handler for component. |
||||
*/ |
||||
onClick: Function, |
||||
} |
||||
|
||||
type State = { |
||||
|
||||
/** |
||||
* The audio level. |
||||
*/ |
||||
level: number, |
||||
} |
||||
|
||||
/** |
||||
* React {@code Component} representing an entry for the microphone audio settings. |
||||
* |
||||
* @param {Props} props - The props of the component. |
||||
* @returns { ReactElement} |
||||
*/ |
||||
export default class MicrophoneEntry extends Component<Props, State> { |
||||
/** |
||||
* Initializes a new {@code MicrophoneEntry} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
level: -1 |
||||
}; |
||||
this._onClick = this._onClick.bind(this); |
||||
this._updateLevel = this._updateLevel.bind(this); |
||||
} |
||||
|
||||
_onClick: () => void; |
||||
|
||||
/** |
||||
* Click handler for the entry. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onClick() { |
||||
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) { |
||||
this.setState({ |
||||
level: Math.floor(num / 0.125) |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Subscribes to audio level chanages comming from the jitsiTrack. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_startListening() { |
||||
const { jitsiTrack } = this.props; |
||||
|
||||
jitsiTrack && jitsiTrack.on( |
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, |
||||
this._updateLevel); |
||||
} |
||||
|
||||
/** |
||||
* Unsubscribes from chanages comming from the jitsiTrack. |
||||
* |
||||
* @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from. |
||||
* @returns {void} |
||||
*/ |
||||
_stopListening(jitsiTrack) { |
||||
jitsiTrack && jitsiTrack.off( |
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, |
||||
this._updateLevel); |
||||
this.setState({ |
||||
level: -1 |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidUpdate}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidUpdate(prevProps: Props) { |
||||
if (prevProps.jitsiTrack !== this.props.jitsiTrack) { |
||||
this._stopListening(prevProps.jitsiTrack); |
||||
this._startListening(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._startListening(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentWillUnmount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
compmonentWillUnmount() { |
||||
this._stopListening(this.props.jitsiTrack); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { children, hasError, isSelected } = this.props; |
||||
|
||||
return ( |
||||
<div |
||||
className = 'audio-preview-microphone' |
||||
onClick = { this._onClick }> |
||||
<AudioSettingsEntry |
||||
hasError = { hasError } |
||||
isSelected = { isSelected }> |
||||
{children} |
||||
</AudioSettingsEntry> |
||||
<Meter |
||||
className = 'audio-preview-meter-mic' |
||||
isDisabled = { hasError } |
||||
level = { this.state.level } /> |
||||
</div> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,119 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import AudioSettingsEntry from './AudioSettingsEntry'; |
||||
import logger from '../../../logger'; |
||||
import TestButton from './TestButton'; |
||||
|
||||
const TEST_SOUND_PATH = 'sounds/ring.wav'; |
||||
|
||||
/** |
||||
* 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, |
||||
|
||||
/** |
||||
* The deviceId of the speaker. |
||||
*/ |
||||
deviceId: 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. |
||||
* |
||||
* @extends 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); |
||||
} |
||||
|
||||
_onClick: () => void; |
||||
|
||||
/** |
||||
* Click handler for the entry. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onClick() { |
||||
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, deviceId } = this.props; |
||||
|
||||
return ( |
||||
<div |
||||
className = 'audio-preview-speaker' |
||||
onClick = { this._onClick }> |
||||
<AudioSettingsEntry |
||||
isSelected = { isSelected } |
||||
key = { deviceId }> |
||||
{children} |
||||
</AudioSettingsEntry> |
||||
<TestButton onClick = { this._onTestButtonClick } /> |
||||
<audio |
||||
preload = 'auto' |
||||
ref = { this.audioRef } |
||||
src = { TEST_SOUND_PATH } /> |
||||
</div> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Click handler for the button. |
||||
*/ |
||||
onClick: Function, |
||||
}; |
||||
|
||||
/** |
||||
* React {@code Component} representing an button used for testing output sound. |
||||
* |
||||
* @returns { ReactElement} |
||||
*/ |
||||
export default function TestButton({ onClick }: Props) { |
||||
return ( |
||||
<div |
||||
className = 'audio-preview-test-button' |
||||
onClick = { onClick }> |
||||
Test |
||||
</div> |
||||
); |
||||
} |
@ -1,2 +1,4 @@ |
||||
export { default as SettingsButton } from './SettingsButton'; |
||||
export { default as SettingsDialog } from './SettingsDialog'; |
||||
export { default as AudioSettingsPopup } from './audio/AudioSettingsPopup'; |
||||
export { default as VideoSettingsPopup } from './video/VideoSettingsPopup'; |
||||
|
@ -0,0 +1,220 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
import { equals } from '../../../../base/redux'; |
||||
import Video from '../../../../base/media/components/Video'; |
||||
import { createLocalVideoTracks } from '../../../functions'; |
||||
|
||||
|
||||
const videoClassName = 'video-preview-video flipVideoX'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link VideoSettingsContent}. |
||||
*/ |
||||
export type Props = { |
||||
|
||||
/** |
||||
* The deviceId of the camera device currently being used. |
||||
*/ |
||||
currentCameraDeviceId: string, |
||||
|
||||
/** |
||||
* Callback invoked to change current camera. |
||||
*/ |
||||
setVideoInputDevice: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* Callback invoked to toggle the settings popup visibility. |
||||
*/ |
||||
toggleVideoSettings: Function, |
||||
|
||||
/** |
||||
* All the camera device ids currently connected. |
||||
*/ |
||||
videoDeviceIds: string[], |
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of {@link VideoSettingsContent}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* An array of all the jitsiTracks and eventual errors. |
||||
*/ |
||||
trackData: Object[], |
||||
}; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displays a list of video |
||||
* previews to choose from. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class VideoSettingsContent extends Component<Props, State> { |
||||
_componentWasUnmounted: boolean; |
||||
|
||||
/** |
||||
* Initializes a new {@code VideoSettingsContent} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
trackData: new Array(props.videoDeviceIds.length).fill({ |
||||
jitsiTrack: null |
||||
}) |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Creates and updates the track data. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
async _setTracks() { |
||||
this._disposeTracks(this.state.trackData); |
||||
|
||||
const trackData = await createLocalVideoTracks( |
||||
this.props.videoDeviceIds, |
||||
); |
||||
|
||||
// In case the component gets unmounted before the tracks are created
|
||||
// avoid a leak by not setting the state
|
||||
if (this._componentWasUnmounted) { |
||||
this._disposeTracks(trackData); |
||||
} else { |
||||
this.setState({ |
||||
trackData |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Destroys all the tracks from trackData object. |
||||
* |
||||
* @param {Object[]} trackData - An array of tracks that are to be disposed. |
||||
* @returns {Promise<void>} |
||||
*/ |
||||
_disposeTracks(trackData) { |
||||
trackData.forEach(({ jitsiTrack }) => { |
||||
jitsiTrack && jitsiTrack.dispose(); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Returns the click handler used when selecting the video preview. |
||||
* |
||||
* @param {string} deviceId - The id of the camera device. |
||||
* @returns {Function} |
||||
*/ |
||||
_onEntryClick(deviceId) { |
||||
return () => { |
||||
this.props.setVideoInputDevice(deviceId); |
||||
this.props.toggleVideoSettings(); |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Renders a preview entry. |
||||
* |
||||
* @param {Object} data - The track data. |
||||
* @param {number} index - The index of the entry. |
||||
* @returns {React$Node} |
||||
*/ |
||||
_renderPreviewEntry(data, index) { |
||||
const { error, jitsiTrack, deviceId } = data; |
||||
const { currentCameraDeviceId, t } = this.props; |
||||
const isSelected = deviceId === currentCameraDeviceId; |
||||
const key = `vp-${index}`; |
||||
const className = 'video-preview-entry'; |
||||
|
||||
if (error) { |
||||
return ( |
||||
<div |
||||
className = { className } |
||||
key = { key }> |
||||
<div className = 'video-preview-error'>{t(error)}</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
const props: Object = { |
||||
className, |
||||
key |
||||
}; |
||||
|
||||
if (isSelected) { |
||||
props.className = `${className} video-preview-entry--selected`; |
||||
} else { |
||||
props.onClick = this._onEntryClick(deviceId); |
||||
} |
||||
|
||||
return ( |
||||
<div { ...props }> |
||||
<div className = 'video-preview-overlay' /> |
||||
<Video |
||||
className = { videoClassName } |
||||
videoTrack = {{ jitsiTrack }} /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._setTracks(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentWillUnmount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
this._componentWasUnmounted = true; |
||||
this._disposeTracks(this.state.trackData); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidUpdate}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidUpdate(prevProps) { |
||||
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) { |
||||
this._setTracks(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { trackData } = this.state; |
||||
|
||||
return ( |
||||
<div> |
||||
{trackData.map((data, i) => this._renderPreviewEntry(data, i))} |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
export default translate(VideoSettingsContent); |
@ -0,0 +1,85 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import InlineDialog from '@atlaskit/inline-dialog'; |
||||
|
||||
import { toggleVideoSettings } from '../../../actions'; |
||||
import { |
||||
getVideoDeviceIds, |
||||
setVideoInputDevice as setVideoInputDeviceAction |
||||
} from '../../../../base/devices'; |
||||
import { getVideoSettingsVisibility } from '../../../functions'; |
||||
import { connect } from '../../../../base/redux'; |
||||
import { getCurrentCameraDeviceId } from '../../../../base/settings'; |
||||
import VideoSettingsContent, { type Props as VideoSettingsProps } from './VideoSettingsContent'; |
||||
|
||||
|
||||
type Props = VideoSettingsProps & { |
||||
|
||||
/** |
||||
* Component children (the Video button). |
||||
*/ |
||||
children: React$Node, |
||||
|
||||
/** |
||||
* Flag controlling the visibility of the popup. |
||||
*/ |
||||
isOpen: boolean, |
||||
|
||||
/** |
||||
* Callback executed when the popup closes. |
||||
*/ |
||||
onClose: Function, |
||||
} |
||||
|
||||
/** |
||||
* Popup with a preview of all the video devices. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
function VideoSettingsPopup({ |
||||
currentCameraDeviceId, |
||||
children, |
||||
isOpen, |
||||
onClose, |
||||
setVideoInputDevice, |
||||
videoDeviceIds |
||||
}: Props) { |
||||
return ( |
||||
<div className = 'video-preview'> |
||||
<InlineDialog |
||||
content = { <VideoSettingsContent |
||||
currentCameraDeviceId = { currentCameraDeviceId } |
||||
setVideoInputDevice = { setVideoInputDevice } |
||||
toggleVideoSettings = { onClose } |
||||
videoDeviceIds = { videoDeviceIds } /> } |
||||
isOpen = { isOpen } |
||||
onClose = { onClose } |
||||
position = 'top right'> |
||||
{ children } |
||||
</InlineDialog> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated {@code VideoSettingsPopup}'s |
||||
* props. |
||||
* |
||||
* @param {Object} state - Redux state. |
||||
* @returns {Object} |
||||
*/ |
||||
function mapStateToProps(state) { |
||||
return { |
||||
currentCameraDeviceId: getCurrentCameraDeviceId(state), |
||||
isOpen: getVideoSettingsVisibility(state), |
||||
videoDeviceIds: getVideoDeviceIds(state) |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
onClose: toggleVideoSettings, |
||||
setVideoInputDevice: setVideoInputDeviceAction |
||||
}; |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoSettingsPopup); |
@ -0,0 +1,127 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import AudioMuteButton from '../AudioMuteButton'; |
||||
import { hasAvailableDevices } from '../../../base/devices'; |
||||
import { IconArrowDown } from '../../../base/icons'; |
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_'; |
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox'; |
||||
import { connect } from '../../../base/redux'; |
||||
|
||||
import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Click handler for the small icon. Opens audio options. |
||||
*/ |
||||
onAudioOptionsClick: Function, |
||||
|
||||
/** |
||||
* If the user has audio input or audio output devices. |
||||
*/ |
||||
hasDevices: boolean, |
||||
|
||||
/** |
||||
* Flag controlling the visibility of the button. |
||||
*/ |
||||
visible: boolean, |
||||
}; |
||||
|
||||
type State = { |
||||
|
||||
/** |
||||
* If there are permissions for audio devices. |
||||
*/ |
||||
hasPermissions: boolean, |
||||
} |
||||
|
||||
/** |
||||
* Button used for audio & audio settings. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
class AudioSettingsButton extends Component<Props, State> { |
||||
/** |
||||
* Initializes a new {@code AudioSettingsButton} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
hasPermissions: false |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Updates device permissions. |
||||
* |
||||
* @returns {Promise<void>} |
||||
*/ |
||||
async _updatePermissions() { |
||||
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted( |
||||
'audio', |
||||
); |
||||
|
||||
this.setState({ |
||||
hasPermissions |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._updatePermissions(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { hasDevices, onAudioOptionsClick, visible } = this.props; |
||||
const settingsDisabled = !this.state.hasPermissions || !hasDevices; |
||||
|
||||
return visible ? ( |
||||
<AudioSettingsPopup> |
||||
<ToolboxButtonWithIcon |
||||
icon = { IconArrowDown } |
||||
iconDisabled = { settingsDisabled } |
||||
onIconClick = { onAudioOptionsClick }> |
||||
<AudioMuteButton /> |
||||
</ToolboxButtonWithIcon> |
||||
</AudioSettingsPopup> |
||||
) : null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Function that maps parts of Redux state tree into component props. |
||||
* |
||||
* @param {Object} state - Redux state. |
||||
* @returns {Object} |
||||
*/ |
||||
function mapStateToProps(state) { |
||||
return { |
||||
hasDevices: |
||||
hasAvailableDevices(state, 'audioInput') |
||||
|| hasAvailableDevices(state, 'audioOutput') |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
onAudioOptionsClick: toggleAudioSettings |
||||
}; |
||||
|
||||
export default connect( |
||||
mapStateToProps, |
||||
mapDispatchToProps, |
||||
)(AudioSettingsButton); |
@ -0,0 +1,124 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings'; |
||||
import VideoMuteButton from '../VideoMuteButton'; |
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_'; |
||||
import { hasAvailableDevices } from '../../../base/devices'; |
||||
import { IconArrowDown } from '../../../base/icons'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Click handler for the small icon. Opens video options. |
||||
*/ |
||||
onVideoOptionsClick: Function, |
||||
|
||||
/** |
||||
* If the user has any video devices. |
||||
*/ |
||||
hasDevices: boolean, |
||||
|
||||
/** |
||||
* Flag controlling the visibility of the button. |
||||
*/ |
||||
visible: boolean, |
||||
}; |
||||
|
||||
type State = { |
||||
|
||||
/** |
||||
* Whether the app has video permissions or not. |
||||
*/ |
||||
hasPermissions: boolean, |
||||
}; |
||||
|
||||
/** |
||||
* Button used for video & video settings. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
class VideoSettingsButton extends Component<Props, State> { |
||||
/** |
||||
* Initializes a new {@code VideoSettingsButton} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
hasPermissions: false |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Updates device permissions. |
||||
* |
||||
* @returns {Promise<void>} |
||||
*/ |
||||
async _updatePermissions() { |
||||
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted( |
||||
'video', |
||||
); |
||||
|
||||
this.setState({ |
||||
hasPermissions |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._updatePermissions(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { hasDevices, onVideoOptionsClick, visible } = this.props; |
||||
const iconDisabled = !this.state.hasPermissions || !hasDevices; |
||||
|
||||
return visible ? ( |
||||
<VideoSettingsPopup> |
||||
<ToolboxButtonWithIcon |
||||
icon = { IconArrowDown } |
||||
iconDisabled = { iconDisabled } |
||||
onIconClick = { onVideoOptionsClick }> |
||||
<VideoMuteButton /> |
||||
</ToolboxButtonWithIcon> |
||||
</VideoSettingsPopup> |
||||
) : null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Function that maps parts of Redux state tree into component props. |
||||
* |
||||
* @param {Object} state - Redux state. |
||||
* @returns {Object} |
||||
*/ |
||||
function mapStateToProps(state) { |
||||
return { |
||||
hasDevices: hasAvailableDevices(state, 'videoInput') |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
onVideoOptionsClick: toggleVideoSettings |
||||
}; |
||||
|
||||
export default connect( |
||||
mapStateToProps, |
||||
mapDispatchToProps, |
||||
)(VideoSettingsButton); |
Loading…
Reference in new issue