feat(toolbox): implement buttons using ToolboxItem

Currently the following are implemented:

- AudioMuteButton
- HangupButton
- VideoMuteButton

In order to implement these new buttons a new abstract class was introduced,
which abstracts the ToolboxItem into a button with enough hooks so a stateful
and a stateless version of it can be created.

This patch only adds the stateful implementation of the aforementioned buttons.
This commit is contained in:
Saúl Ibarra Corretgé 2018-04-13 15:03:12 +02:00 committed by Lyubo Marinov
parent 8d94cc5cb2
commit b634f6b200
15 changed files with 572 additions and 828 deletions

View File

@ -14,6 +14,7 @@ import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../base/responsive-ui';
import { ColorPalette } from '../../base/styles';
import { InviteButton } from '../../invite';
import {
EnterPictureInPictureToolbarButton
@ -159,6 +160,11 @@ class Toolbox extends Component<Props> {
_renderPrimaryToolbar() {
const audioButtonStyles = this._getMuteButtonStyles(MEDIA_TYPE.AUDIO);
const videoButtonStyles = this._getMuteButtonStyles(MEDIA_TYPE.VIDEO);
const hangupButtonStyles = {
iconStyle: styles.whitePrimaryToolbarButtonIcon,
style: styles.hangup,
underlayColor: ColorPalette.buttonUnderlay
};
/* eslint-disable react/jsx-handler-names */
@ -168,7 +174,7 @@ class Toolbox extends Component<Props> {
pointerEvents = 'box-none'
style = { styles.primaryToolbar }>
<AudioMuteButton styles = { audioButtonStyles } />
<HangupButton />
<HangupButton styles = { hangupButtonStyles } />
<VideoMuteButton styles = { videoButtonStyles } />
</View>
);

View File

@ -354,12 +354,12 @@ class Toolbox extends Component<Props, State> {
</div> }
</div>
<div className = 'button-group-center'>
{ this._shouldShowButton('microphone')
&& <AudioMuteButton /> }
{ this._shouldShowButton('hangup')
&& <HangupButton /> }
{ this._shouldShowButton('camera')
&& <VideoMuteButton /> }
<AudioMuteButton
visible = { this._shouldShowButton('microphone') } />
<HangupButton
visible = { this._shouldShowButton('hangup') } />
<VideoMuteButton
visible = { this._shouldShowButton('camera') } />
</div>
<div className = 'button-group-right'>
{ this._shouldShowButton('invite')

View File

@ -1,87 +1,62 @@
// @flow
import PropTypes from 'prop-types';
import { Component } from 'react';
import {
AUDIO_MUTE,
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import {
VIDEO_MUTISM_AUTHORITY,
setAudioMuted
} from '../../../base/media';
import AbstractButton from './AbstractButton';
import type { Props } from './AbstractButton';
/**
* An abstract implementation of a button for toggling audio mute.
*/
export default class AbstractAudioMuteButton extends Component<*> {
/**
* {@code AbstractAudioMuteButton} component's property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the local microphone is muted.
*/
_audioMuted: PropTypes.bool,
/**
* Invoked to toggle audio mute.
*/
dispatch: PropTypes.func
};
class AbstractAudioMuteButton<P: Props, S: *> extends AbstractButton<P, S> {
accessibilityLabel = 'Audio mute';
iconName = 'icon-microphone';
toggledIconName = 'icon-mic-disabled toggled';
/**
* Initializes a new {@code AbstractAudioMuteButton} instance.
*
* @param {Props} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind event handler so it is only bound once per instance.
this._onToolbarToggleAudio = this._onToolbarToggleAudio.bind(this);
}
/**
* Dispatches an action to toggle audio mute.
* Handles clicking / pressing the button, and toggles the audio mute state
* accordingly.
*
* @override
* @private
* @returns {void}
*/
_doToggleAudio() {
// The user sees the reality i.e. the state of base/tracks and intends
// to change reality by tapping on the respective button i.e. the user
// sets the state of base/media. Whether the user's intention will turn
// into reality is a whole different story which is of no concern to the
// tapping.
this.props.dispatch(
setAudioMuted(
!this.props._audioMuted,
VIDEO_MUTISM_AUTHORITY.USER,
/* ensureTrack */ true));
_handleClick() {
this._setAudioMuted(!this._isAudioMuted());
}
_onToolbarToggleAudio: () => void;
/**
* Helper function to be implemented by subclasses, which must return a
* boolean value indicating if audio is muted or not.
*
* @abstract
* @private
* @returns {boolean}
*/
_isAudioMuted() {
// To be implemented by subclass.
}
/**
* Creates an analytics toolbar event and dispatches an action for toggling
* audio mute.
* Indicates whether this button is in toggled state or not.
*
* @override
* @private
* @returns {boolean}
*/
_isToggled() {
return this._isAudioMuted();
}
/**
* Helper function to perform the actual setting of the audio mute / unmute
* action.
*
* @param {boolean} audioMuted - Whether video should be muted or not.
* @private
* @returns {void}
*/
_onToolbarToggleAudio() {
sendAnalytics(createToolbarEvent(
AUDIO_MUTE,
{
enable: !this.props._audioMuted
}));
this._doToggleAudio();
_setAudioMuted(audioMuted: boolean) { // eslint-disable-line no-unused-vars
// To be implemented by subclass.
}
}
export default AbstractAudioMuteButton;

View File

@ -0,0 +1,195 @@
// @flow
import React, { Component } from 'react';
import ToolboxItem from '../ToolboxItem';
import type { Styles } from '../AbstractToolboxItem';
export type Props = {
/**
* Whether to show the label or not.
*/
showLabel: boolean,
/**
* Collection of styles for the button.
*/
styles: ?Styles,
/**
* Collection of styles for the button, when in toggled state.
*/
toggledStyles: ?Styles,
/**
* From which direction the tooltip should appear, relative to the
* button.
*/
tooltipPosition: string,
/**
* Whether this button is visible or not.
*/
visible: boolean
};
/**
* An abstract implementation of a button.
*/
export default class AbstractButton<P: Props, S : *> extends Component<P, S> {
static defaultProps = {
showLabel: false,
styles: undefined,
toggledStyles: undefined,
tooltipPosition: 'top',
visible: true
};
/**
* A succinct description of what the button does. Used by accessibility
* tools and torture tests.
*
* @abstract
*/
accessibilityLabel: string;
/**
* The name of the icon of this button.
*
* @abstract
*/
iconName: string;
/**
* The text associated with this button. When `showLabel` is set to
* {@code true}, it will be displayed alongside the icon.
*
* @abstract
*/
label: string;
/**
* The name of the icon of this button, when toggled.
*
* @abstract
*/
toggledIconName: string;
/**
* The text to display in the tooltip. Used only on web.
*
* @abstract
*/
tooltip: string;
/**
* Initializes a new {@code AbstractButton} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code AbstractAudioMuteButton} instance with.
*/
constructor(props: P) {
super(props);
this._onClick = this._onClick.bind(this);
}
/**
* Helper function to be implemented by subclasses, which should be used
* to handle the button being clicked / pressed.
*
* @abstract
* @private
* @returns {void}
*/
_handleClick() {
// To be implemented by subclass.
}
/**
* Gets the current icon name, taking the toggled state into account. If no
* toggled icon is provided, the regular icon will also be used in the
* toggled state.
*
* @private
* @returns {string}
*/
_getIconName() {
return (this._isToggled() ? this.toggledIconName : this.iconName)
|| this.iconName;
}
/**
* Gets the current styles, taking the toggled state into account. If no
* toggled styles are provided, the regular styles will also be used in the
* toggled state.
*
* @private
* @returns {?Styles}
*/
_getStyles() {
const { styles, toggledStyles } = this.props;
return (this._isToggled() ? toggledStyles : styles) || styles;
}
/**
* Helper function to be implemented by subclasses, which must return a
* boolean value indicating if this button is disabled or not.
*
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
/**
* Helper function to be implemented by subclasses, which must return a
* boolean value indicating if this button is toggled or not.
*
* @private
* @returns {boolean}
*/
_isToggled() {
return false;
}
_onClick: (*) => void;
/**
* Handles clicking / pressing the button, and toggles the audio mute state
* accordingly.
*
* @private
* @returns {void}
*/
_onClick() {
this._handleClick();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const props = {
...this.props,
accessibilityLabel: this.accessibilityLabel,
iconName: this._getIconName(),
label: this.label,
styles: this._getStyles(),
tooltip: this.tooltip
};
return (
<ToolboxItem
disabled = { this._isDisabled() }
onClick = { this._onClick }
{ ...props } />
);
}
}

View File

@ -1,51 +1,35 @@
// @flow
import { Component } from 'react';
import {
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import AbstractButton from './AbstractButton';
import type { Props } from './AbstractButton';
/**
* An abstract implementation of a button for leaving the conference.
* An abstract implementation of a button for disconnecting a conference.
*/
export default class AbstractHangupButton extends Component<*> {
/**
* Initializes a new {@code AbstractHangupButton} instance.
*
* @param {Props} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: Object) {
super(props);
class AbstractHangupButton<P : Props, S: *> extends AbstractButton<P, S> {
accessibilityLabel = 'Hangup';
iconName = 'icon-hangup';
// Bind event handler so it is only bound once per instance.
this._onToolbarHangup = this._onToolbarHangup.bind(this);
/**
* Handles clicking / pressing the button, and disconnects the conference.
*
* @private
* @returns {void}
*/
_handleClick() {
this._doHangup();
}
/**
* Dispatches an action for leaving the current conference.
* Helper function to perform the actual hangup action.
*
* @abstract
* @private
* @returns {void}
*/
_doHangup() {
/* to be implemented by descendants */
}
_onToolbarHangup: () => void;
/**
* Creates an analytics toolbar event and dispatches an action for leaving
* the current conference.
*
* @private
* @returns {void}
*/
_onToolbarHangup() {
sendAnalytics(createToolbarEvent('hangup'));
this._doHangup();
// To be implemented by subclass.
}
}
export default AbstractHangupButton;

View File

@ -1,88 +1,61 @@
// @flow
import PropTypes from 'prop-types';
import { Component } from 'react';
import {
VIDEO_MUTE,
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import {
VIDEO_MUTISM_AUTHORITY,
setVideoMuted
} from '../../../base/media';
import AbstractButton from './AbstractButton';
import type { Props } from './AbstractButton';
/**
* An abstract implementation of a button for toggling video mute.
*/
export default class AbstractVideoMuteButton extends Component<*> {
/**
* {@code AbstractVideoMuteButton} component's property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the local camera is muted.
*/
_videoMuted: PropTypes.bool,
/**
* Invoked to toggle video mute.
*/
dispatch: PropTypes.func
};
class AbstractVideoMuteButton<P : Props, S : *> extends AbstractButton<P, S> {
accessibilityLabel = 'Video mute';
iconName = 'icon-camera';
toggledIconName = 'icon-camera-disabled toggled';
/**
* Initializes a new {@code AbstractVideoMuteButton} instance.
*
* @param {Props} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind event handler so it is only bound once per instance.
this._onToolbarToggleVideo = this._onToolbarToggleVideo.bind(this);
}
/**
* Dispatches an action to toggle the mute state of the video/camera.
* Handles clicking / pressing the button, and toggles the video mute state
* accordingly.
*
* @private
* @returns {void}
*/
_doToggleVideo() {
// The user sees the reality i.e. the state of base/tracks and intends
// to change reality by tapping on the respective button i.e. the user
// sets the state of base/media. Whether the user's intention will turn
// into reality is a whole different story which is of no concern to the
// tapping.
this.props.dispatch(
setVideoMuted(
!this.props._videoMuted,
VIDEO_MUTISM_AUTHORITY.USER,
/* ensureTrack */ true));
_handleClick() {
this._setVideoMuted(!this._isVideoMuted());
}
_onToolbarToggleVideo: () => void;
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @private
* @returns {boolean}
*/
_isToggled() {
return this._isVideoMuted();
}
/**
* Creates an analytics toolbar event and dispatches an action for toggling
* video mute.
* Helper function to be implemented by subclasses, which must return a
* boolean value indicating if video is muted or not.
*
* @abstract
* @private
* @returns {boolean}
*/
_isVideoMuted() {
// To be implemented by subclass.
}
/**
* Helper function to perform the actual setting of the video mute / unmute
* action.
*
* @param {boolean} videoMuted - Whether video should be muted or not.
* @private
* @returns {void}
*/
_onToolbarToggleVideo() {
sendAnalytics(createToolbarEvent(
VIDEO_MUTE,
{
enable: !this.props._videoMuted
}));
this._doToggleVideo();
_setVideoMuted(videoMuted: boolean) { // eslint-disable-line no-unused-vars
// To be implemented by subclass.
}
}
export default AbstractVideoMuteButton;

View File

@ -0,0 +1,96 @@
// @flow
import { connect } from 'react-redux';
import {
AUDIO_MUTE,
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import {
MEDIA_TYPE,
setAudioMuted
} from '../../../base/media';
import { isLocalTrackMuted } from '../../../base/tracks';
import AbstractAudioMuteButton from './AbstractAudioMuteButton';
import type { Props as AbstractButtonProps } from './AbstractButton';
type Props = AbstractButtonProps & {
/**
* Whether audio is currently muted or not.
*/
_audioMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* Component that renders a toolbar button for toggling audio mute.
*
* @extends AbstractAudioMuteButton
*/
class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
label = 'toolbar.mute';
tooltip = 'toolbar.mute';
/**
* Indicates if this button should be disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
/**
* Indicates if audio is currently muted ot nor.
*
* @override
* @private
* @returns {boolean}
*/
_isAudioMuted() {
return this.props._audioMuted;
}
/**
* Changes the muted state.
*
* @param {boolean} audioMuted - Whether audio should be muted or not.
* @private
* @returns {void}
*/
_setAudioMuted(audioMuted: boolean) {
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable: audioMuted }));
this.props.dispatch(setAudioMuted(audioMuted));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioMuted: boolean
* }}
*/
function _mapStateToProps(state): Object {
const tracks = state['features/base/tracks'];
return {
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO)
};
}
export default translate(connect(_mapStateToProps)(AudioMuteButton));

View File

@ -1,73 +0,0 @@
// @flow
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { MEDIA_TYPE } from '../../../base/media';
import { isLocalTrackMuted } from '../../../base/tracks';
import AbstractAudioMuteButton from './AbstractAudioMuteButton';
import ToolbarButton from '../ToolbarButton';
/**
* Component that renders a toolbar button for toggling audio mute.
*
* @extends AbstractAudioMuteButton
*/
export class AudioMuteButton extends AbstractAudioMuteButton {
/**
* {@code AbstractAudioMuteButton} component's property types.
*
* @static
*/
static propTypes = {
...AbstractAudioMuteButton.propTypes,
/**
* Styles to be applied to the button and the icon to show.
*/
buttonStyles: PropTypes.object
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { buttonStyles } = this.props;
return (
<ToolbarButton
iconName = { buttonStyles.iconName }
iconStyle = { buttonStyles.iconStyle }
onClick = { this._onToolbarToggleAudio }
style = { buttonStyles.style } />
);
}
_onToolbarToggleAudio: () => void;
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code AudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioMuted: boolean,
* }}
*/
function _mapStateToProps(state) {
const tracks = state['features/base/tracks'];
return {
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO)
};
}
export default connect(_mapStateToProps)(AudioMuteButton);

View File

@ -1,181 +0,0 @@
// @flow
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import UIEvents from '../../../../../service/UI/UIEvents';
import {
ACTION_SHORTCUT_TRIGGERED,
AUDIO_MUTE,
createShortcutEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import { MEDIA_TYPE } from '../../../base/media';
import { isLocalTrackMuted } from '../../../base/tracks';
import AbstractAudioMuteButton from './AbstractAudioMuteButton';
import ToolbarButton from '../ToolbarButton';
declare var APP: Object;
/**
* Component that renders a toolbar button for toggling audio mute.
*
* @extends Component
*/
export class AudioMuteButton extends AbstractAudioMuteButton {
/**
* Default values for {@code AudioMuteButton} component's properties.
*
* @static
*/
static defaultProps = {
tooltipPosition: 'top'
};
/**
* {@code AudioMuteButton} component's property types.
*
* @static
*/
static propTypes = {
...AbstractAudioMuteButton.propTypes,
/**
* The {@code JitsiConference} for the current conference.
*/
_conference: PropTypes.object,
/**
* Invoked to update the audio mute status.
*/
dispatch: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* Where the tooltip should display, relative to the button.
*/
tooltipPosition: PropTypes.string
};
/**
* Initializes a new {@code AudioMuteButton} instance.
*
* @param {Props} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind event handlers so it is only bound once per instance.
this._onShortcutToggleAudio = this._onShortcutToggleAudio.bind(this);
}
/**
* Sets a keyboard shortcuts for toggling audio mute.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
APP.keyboardshortcut.registerShortcut(
'M',
null,
this._onShortcutToggleAudio,
'keyboardShortcuts.mute');
}
/**
* Removes the registered keyboard shortcut handler.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
APP.keyboardshortcut.unregisterShortcut('M');
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _audioMuted, _conference, t, tooltipPosition } = this.props;
return (
<ToolbarButton
accessibilityLabel = 'Audio mute'
iconName = { _audioMuted && _conference
? 'icon-mic-disabled toggled'
: 'icon-microphone' }
onClick = { this._onToolbarToggleAudio }
tooltip = { t('toolbar.mute') }
tooltipPosition = { tooltipPosition } />
);
}
_doToggleAudio: () => void;
/**
* Emits an event to signal audio mute should be toggled.
*
* @private
* @returns {void}
*/
_doToggleAudio() {
// The old conference logic must be used for now as the redux flows do
// not handle all cases, such as unmuting when the config
// startWithAudioMuted is true.
APP.UI.emitEvent(UIEvents.AUDIO_MUTED, !this.props._audioMuted, true);
}
_onShortcutToggleAudio: () => void;
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling audio mute.
*
* @private
* @returns {void}
*/
_onShortcutToggleAudio() {
sendAnalytics(createShortcutEvent(
AUDIO_MUTE,
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this.props._audioMuted }));
this._doToggleAudio();
}
_onToolbarToggleAudio: () => void;
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code AudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioMuted: boolean,
* _conference: Object,
* }}
*/
function _mapStateToProps(state) {
const tracks = state['features/base/tracks'];
return {
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO),
_conference: state['features/base/conference'].conference
};
}
export default translate(connect(_mapStateToProps)(AudioMuteButton));

View File

@ -0,0 +1,61 @@
// @flow
import { connect } from 'react-redux';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { appNavigate } from '../../../app';
import { disconnect } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import AbstractHangupButton from './AbstractHangupButton';
import type { Props as AbstractButtonProps } from './AbstractButton';
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* Component that renders a toolbar button for leaving the current conference.
*
* @extends AbstractHangupButton
*/
class HangupButton extends AbstractHangupButton<Props, *> {
label = 'toolbar.hangup';
tooltip = 'toolbar.hangup';
/**
* Helper function to perform the actual hangup action.
*
* @override
* @private
* @returns {void}
*/
_doHangup() {
sendAnalytics(createToolbarEvent('hangup'));
// FIXME: these should be unified.
if (navigator.product === 'ReactNative') {
this.props.dispatch(appNavigate(undefined));
} else {
this.props.dispatch(disconnect(true));
}
}
/**
* Indicates if this button should be disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
}
export default translate(connect()(HangupButton));

View File

@ -1,63 +0,0 @@
// @flow
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { appNavigate } from '../../../app';
import { ColorPalette } from '../../../base/styles';
import AbstractHangupButton from './AbstractHangupButton';
import ToolbarButton from '../ToolbarButton';
import styles from '../styles';
/**
* Component that renders a toolbar button for leaving the current conference.
*
* @extends Component
*/
class HangupButton extends AbstractHangupButton {
/**
* {@code HangupButton} component's property types.
*
* @static
*/
static propTypes = {
/**
* Invoked to leave the conference.
*/
dispatch: PropTypes.func
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<ToolbarButton
accessibilityLabel = 'Hangup'
iconName = 'hangup'
iconStyle = { styles.whitePrimaryToolbarButtonIcon }
onClick = { this._onToolbarHangup }
style = { styles.hangup }
underlayColor = { ColorPalette.buttonUnderlay } />
);
}
/**
* Dispatches an action for leaving the current conference.
*
* @private
* @returns {void}
*/
_doHangup() {
this.props.dispatch(appNavigate(undefined));
}
_onToolbarHangup: () => void;
}
export default connect()(HangupButton);

View File

@ -1,83 +0,0 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { disconnect } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import AbstractHangupButton from './AbstractHangupButton';
import ToolbarButton from '../ToolbarButton';
/**
* Component that renders a toolbar button for leaving the current conference.
*
* @extends Component
*/
export class HangupButton extends AbstractHangupButton {
/**
* Default values for {@code HangupButton} component's properties.
*
* @static
*/
static defaultProps = {
tooltipPosition: 'top'
};
/**
* {@code HangupButton} component's property types.
*
* @static
*/
static propTypes = {
/**
* Invoked to trigger conference leave.
*/
dispatch: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* Where the tooltip should display, relative to the button.
*/
tooltipPosition: PropTypes.string
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t, tooltipPosition } = this.props;
return (
<ToolbarButton
accessibilityLabel = 'Hangup'
iconName = 'icon-hangup'
onClick = { this._onToolbarHangup }
tooltip = { t('toolbar.hangup') }
tooltipPosition = { tooltipPosition } />
);
}
_onToolbarHangup: () => void;
/**
* Dispatches an action for leaving the current conference.
*
* @private
* @returns {void}
*/
_doHangup() {
this.props.dispatch(disconnect(true));
}
}
export default translate(connect()(HangupButton));

View File

@ -0,0 +1,109 @@
// @flow
import { connect } from 'react-redux';
import {
VIDEO_MUTE,
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import {
MEDIA_TYPE,
VIDEO_MUTISM_AUTHORITY,
setVideoMuted
} from '../../../base/media';
import { isLocalTrackMuted } from '../../../base/tracks';
import AbstractVideoMuteButton from './AbstractVideoMuteButton';
import type { Props as AbstractButtonProps } from './AbstractButton';
type Props = AbstractButtonProps & {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean,
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* Component that renders a toolbar button for toggling video mute.
*
* @extends AbstractVideoMuteButton
*/
class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
label = 'toolbar.videomute';
tooltip = 'toolbar.videomute';
/**
* Indicates if this button should be disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return this.props._audioOnly;
}
/**
* Indicates if video is currently muted ot nor.
*
* @override
* @private
* @returns {boolean}
*/
_isVideoMuted() {
return this.props._videoMuted;
}
/**
* Changes the muted state.
*
* @param {boolean} videoMuted - Whether video should be muted or not.
* @private
* @returns {void}
*/
_setVideoMuted(videoMuted: boolean) {
sendAnalytics(createToolbarEvent(VIDEO_MUTE, { enable: videoMuted }));
this.props.dispatch(
setVideoMuted(
videoMuted,
VIDEO_MUTISM_AUTHORITY.USER,
/* ensureTrack */ true));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code VideoMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean,
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state): Object {
const { audioOnly } = state['features/base/conference'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}
export default translate(connect(_mapStateToProps)(VideoMuteButton));

View File

@ -1,82 +0,0 @@
// @flow
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { MEDIA_TYPE } from '../../../base/media';
import { isLocalTrackMuted } from '../../../base/tracks';
import AbstractVideoMuteButton from './AbstractVideoMuteButton';
import ToolbarButton from '../ToolbarButton';
/**
* Component that renders a toolbar button for toggling video mute.
*
* @extends AbstractVideoMuteButton
*/
class VideoMuteButton extends AbstractVideoMuteButton {
/**
* {@code VideoMuteButton} component's property types.
*
* @static
*/
static propTypes = {
...AbstractVideoMuteButton.propTypes,
/**
* Whether or not the local participant is current in audio only mode.
* Video mute toggling is disabled in audio only mode.
*/
_audioOnly: PropTypes.bool,
/**
* Styles to be applied to the button and the icon to show.
*/
buttonStyles: PropTypes.object
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _audioOnly, buttonStyles } = this.props;
return (
<ToolbarButton
disabled = { _audioOnly }
iconName = { buttonStyles.iconName }
iconStyle = { buttonStyles.iconStyle }
onClick = { this._onToolbarToggleVideo }
style = { buttonStyles.style } />
);
}
_onToolbarToggleVideo: () => void;
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code VideoMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean,
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state) {
const conference = state['features/base/conference'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(conference.audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}
export default connect(_mapStateToProps)(VideoMuteButton);

View File

@ -1,173 +0,0 @@
// @flow
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import UIEvents from '../../../../../service/UI/UIEvents';
import {
ACTION_SHORTCUT_TRIGGERED,
VIDEO_MUTE,
createShortcutEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import { MEDIA_TYPE } from '../../../base/media';
import { isLocalTrackMuted } from '../../../base/tracks';
import AbstractVideoMuteButton from './AbstractVideoMuteButton';
import ToolbarButton from '../ToolbarButton';
declare var APP: Object;
/**
* Component that renders a toolbar button for toggling video mute.
*
* @extends AbstractVideoMuteButton
*/
export class VideoMuteButton extends AbstractVideoMuteButton {
/**
* Default values for {@code VideoMuteButton} component's properties.
*
* @static
*/
static defaultProps = {
tooltipPosition: 'top'
};
/**
* {@code VideoMuteButton} component's property types.
*
* @static
*/
static propTypes = {
...AbstractVideoMuteButton.propTypes,
/**
* The {@code JitsiConference} for the current conference.
*/
_conference: PropTypes.object,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* Where the tooltip should display, relative to the button.
*/
tooltipPosition: PropTypes.string
};
/**
* Initializes a new {@code VideoMuteButton} instance.
*
* @param {Props} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onShortcutToggleVideo = this._onShortcutToggleVideo.bind(this);
}
/**
* Sets a keyboard shortcuts for toggling video mute.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
APP.keyboardshortcut.registerShortcut(
'V',
null,
this._onShortcutToggleVideo,
'keyboardShortcuts.videoMute');
}
/**
* Removes the registered keyboard shortcut handler.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
APP.keyboardshortcut.unregisterShortcut('V');
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _conference, _videoMuted, t, tooltipPosition } = this.props;
return (
<ToolbarButton
accessibilityLabel = 'Video mute'
iconName = { _videoMuted && _conference
? 'icon-camera-disabled toggled'
: 'icon-camera' }
onClick = { this._onToolbarToggleVideo }
tooltip = { t('toolbar.videomute') }
tooltipPosition = { tooltipPosition } />
);
}
_doToggleVideo: () => void;
/**
* Emits an event to signal video mute should be toggled.
*
* @private
* @returns {void}
*/
_doToggleVideo() {
APP.UI.emitEvent(UIEvents.VIDEO_MUTED, !this.props._videoMuted);
}
_onShortcutToggleVideo: () => void;
/**
* Creates an analytics keyboard shortcut event for and dispatches an action
* for toggling video mute.
*
* @private
* @returns {void}
*/
_onShortcutToggleVideo() {
sendAnalytics(createShortcutEvent(
VIDEO_MUTE,
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this.props._videoMuted }));
this._doToggleVideo();
}
_onToolbarToggleVideo: () => void;
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code AudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _conference: Object,
* _videoMuted: boolean,
* }}
*/
function _mapStateToProps(state) {
const tracks = state['features/base/tracks'];
return {
_conference: state['features/base/conference'].conference,
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}
export default translate(connect(_mapStateToProps)(VideoMuteButton));