+ { /* FIXME: onChange and onMouseUp are both used for
+ * compatibility with IE11. This workaround can be
+ * removed after upgrading to React 16.
+ */ }
+
+
+
+
+ { this._createLabels(activeSliderOption) }
+
+
+
+ );
+ }
+
+ /**
+ * Creates React Elements for notifying that peer to peer is enabled.
+ *
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderP2PMessage() {
+ const { t } = this.props;
+
+ return (
+
+ { t('videoStatus.p2pVideoQualityDescription') }
+
+ );
+ }
+
+ /**
+ * Creates React Elements to display mock tick marks with associated labels.
+ *
+ * @param {number} activeLabelIndex - Which of the sliderOptions should
+ * display as currently active.
+ * @private
+ * @returns {ReactElement[]}
+ */
+ _createLabels(activeLabelIndex) {
+ const labelsCount = this._sliderOptions.length;
+ const maxWidthOfLabel = `${100 / labelsCount}%`;
+
+ return this._sliderOptions.map((sliderOption, index) => {
+ const style = {
+ maxWidth: maxWidthOfLabel,
+ left: `${(index * 100) / (labelsCount - 1)}%`
+ };
+
+ const isActiveClass = activeLabelIndex === index ? 'active' : '';
+ const className
+ = `video-quality-dialog-label-container ${isActiveClass}`;
+
+ return (
+
+
+ { this.props.t(sliderOption.textKey) }
+
+
+ );
+ });
+ }
+
+ /**
+ * Dispatches an action to enable audio only mode.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableAudioOnly() {
+ this.props.dispatch(setAudioOnly(true));
+ }
+
+ /**
+ * Dispatches an action to receive high quality video from remote
+ * participants.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableHighDefinition() {
+ this.props.dispatch(setReceiveVideoQuality(HIGH));
+ }
+
+ /**
+ * Dispatches an action to receive low quality video from remote
+ * participants.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableLowDefinition() {
+ this.props.dispatch(setReceiveVideoQuality(LOW));
+ }
+
+ /**
+ * Dispatches an action to receive standard quality video from remote
+ * participants.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableStandardDefinition() {
+ this.props.dispatch(setReceiveVideoQuality(STANDARD));
+ }
+
+ /**
+ * Matches the current video quality state with corresponding index of the
+ * component's slider options.
+ *
+ * @private
+ * @returns {void}
+ */
+ _mapCurrentQualityToSliderValue() {
+ const { _audioOnly, _receiveVideoQuality } = this.props;
+ const { _sliderOptions } = this;
+
+ if (_audioOnly) {
+ const audioOnlyOption = _sliderOptions.find(
+ ({ audioOnly }) => audioOnly);
+
+ return _sliderOptions.indexOf(audioOnlyOption);
+ }
+
+ const matchingOption = _sliderOptions.find(
+ ({ videoQuality }) => videoQuality === _receiveVideoQuality);
+
+ return _sliderOptions.indexOf(matchingOption);
+ }
+
+ /**
+ * Invokes a callback when the selected video quality changes.
+ *
+ * @param {Object} event - The slider's change event.
+ * @private
+ * @returns {void}
+ */
+ _onSliderChange(event) {
+ const { _audioOnly, _receiveVideoQuality } = this.props;
+ const {
+ audioOnly,
+ onSelect,
+ videoQuality
+ } = this._sliderOptions[event.target.value];
+
+ // Take no action if the newly chosen option does not change audio only
+ // or video quality state.
+ if ((_audioOnly && audioOnly)
+ || (!_audioOnly && videoQuality === _receiveVideoQuality)) {
+ return;
+ }
+
+ onSelect();
+ }
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code VideoQualityDialog} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _audioOnly: boolean,
+ * _p2p: boolean,
+ * _receiveVideoQuality: boolean
+ * }}
+ */
+function _mapStateToProps(state) {
+ const {
+ audioOnly,
+ p2p,
+ receiveVideoQuality
+ } = state['features/base/conference'];
+
+ return {
+ _audioOnly: audioOnly,
+ _p2p: p2p,
+ _receiveVideoQuality: receiveVideoQuality
+ };
+}
+
+export default translate(connect(_mapStateToProps)(VideoQualityDialog));
+
diff --git a/react/features/video-quality/components/VideoQualityLabel.native.js b/react/features/video-quality/components/VideoQualityLabel.native.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/video-status-label/components/VideoStatusLabel.js b/react/features/video-quality/components/VideoQualityLabel.web.js
similarity index 52%
rename from react/features/video-status-label/components/VideoStatusLabel.js
rename to react/features/video-quality/components/VideoQualityLabel.web.js
index 2bed11bf8..b4e9ef274 100644
--- a/react/features/video-status-label/components/VideoStatusLabel.js
+++ b/react/features/video-quality/components/VideoQualityLabel.web.js
@@ -1,9 +1,37 @@
+import AKInlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { connect } from 'react-redux';
-import { toggleAudioOnly } from '../../base/conference';
import { translate } from '../../base/i18n';
+import { VideoQualityDialog } from './';
+
+import {
+ VIDEO_QUALITY_LEVELS
+} from '../../base/conference';
+
+const { HIGH, STANDARD, LOW } = VIDEO_QUALITY_LEVELS;
+
+/**
+ * Expected video resolutions placed into an array, sorted from lowest to
+ * highest resolution.
+ *
+ * @type {number[]}
+ */
+const RESOLUTIONS
+ = Object.values(VIDEO_QUALITY_LEVELS).sort((a, b) => a - b);
+
+/**
+ * A map of video resolution (number) to translation key.
+ *
+ * @type {Object}
+ */
+const RESOLUTION_TO_TRANSLATION_KEY = {
+ [HIGH]: 'videoStatus.hd',
+ [STANDARD]: 'videoStatus.sd',
+ [LOW]: 'videoStatus.ld'
+};
+
/**
* React {@code Component} responsible for displaying a label that indicates
* the displayed video state of the current conference. {@code AudioOnlyLabel}
@@ -11,9 +39,9 @@ import { translate } from '../../base/i18n';
* will display if not in audio only mode and a high-definition large video is
* being displayed.
*/
-export class VideoStatusLabel extends Component {
+export class VideoQualityLabel extends Component {
/**
- * {@code VideoStatusLabel}'s property types.
+ * {@code VideoQualityLabel}'s property types.
*
* @static
*/
@@ -34,11 +62,6 @@ export class VideoStatusLabel extends Component {
*/
_filmstripVisible: React.PropTypes.bool,
- /**
- * Whether or not a high-definition large video is displayed.
- */
- _largeVideoHD: React.PropTypes.bool,
-
/**
* Whether or note remote videos are visible in the filmstrip,
* regardless of count. Used to determine display classes to set.
@@ -46,9 +69,9 @@ export class VideoStatusLabel extends Component {
_remoteVideosVisible: React.PropTypes.bool,
/**
- * Invoked to request toggling of audio only mode.
+ * The current video resolution (height) to display a label for.
*/
- dispatch: React.PropTypes.func,
+ _resolution: React.PropTypes.number,
/**
* Invoked to obtain translated strings.
@@ -57,7 +80,7 @@ export class VideoStatusLabel extends Component {
};
/**
- * Initializes a new {@code VideoStatusLabel} instance.
+ * Initializes a new {@code VideoQualityLabel} instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
@@ -66,13 +89,25 @@ export class VideoStatusLabel extends Component {
super(props);
this.state = {
- // Whether or not the filmstrip is transitioning from not visible
- // to visible. Used to set a transition class for animation.
+ /**
+ * Whether or not the {@code VideoQualityDialog} is displayed.
+ *
+ * @type {boolean}
+ */
+ showVideoQualityDialog: false,
+
+ /**
+ * Whether or not the filmstrip is transitioning from not visible
+ * to visible. Used to set a transition class for animation.
+ *
+ * @type {boolean}
+ */
togglingToVisible: false
};
- // Bind event handler so it is only bound once for every instance.
- this._toggleAudioOnly = this._toggleAudioOnly.bind(this);
+ // Bind event handlers so they are only bound once for every instance.
+ this._onDialogClose = this._onDialogClose.bind(this);
+ this._onDialogToggle = this._onDialogToggle.bind(this);
}
/**
@@ -103,8 +138,7 @@ export class VideoStatusLabel extends Component {
_conferenceStarted,
_filmstripVisible,
_remoteVideosVisible,
- _largeVideoHD,
- t
+ _resolution
} = this.props;
// FIXME The _conferenceStarted check is used to be defensive against
@@ -114,15 +148,6 @@ export class VideoStatusLabel extends Component {
return null;
}
- let displayedLabel;
-
- if (_audioOnly) {
- displayedLabel = ;
- } else {
- displayedLabel = _largeVideoHD
- ? t('videoStatus.hd') : t('videoStatus.sd');
- }
-
// Determine which classes should be set on the component. These classes
// will used to help with animations and setting position.
const baseClasses = 'video-state-indicator moveToCorner';
@@ -138,57 +163,77 @@ export class VideoStatusLabel extends Component {
return (
);
}
/**
- * Renders a dropdown menu for changing video modes.
+ * Matches the passed in resolution with a translation key for describing
+ * the resolution. The passed in resolution will be matched with a known
+ * resolution that it is at least greater than or equal to.
*
+ * @param {number} resolution - The video height to match with a
+ * translation.
* @private
- * @returns {ReactElement}
+ * @returns {string}
*/
- _renderVideonMenu() {
- const { _audioOnly, t } = this.props;
- const audioOnlyAttributes = _audioOnly ? { className: 'active' }
- : { onClick: this._toggleAudioOnly };
- const videoAttributes = _audioOnly ? { onClick: this._toggleAudioOnly }
- : { className: 'active' };
+ _mapResolutionToTranslation(resolution) {
+ // Set the default matching resolution of the lowest just in case a
+ // match is not found.
+ let highestMatchingResolution = RESOLUTIONS[0];
- return (
-