feat(remote-video): convert remote video menu to react
- Create new React Components for RemoteVideoMenu and its buttons - Remove existing menu creation from RemoteVideo - Refactor RemoteVideo so all function binding happens once in the constructor, removing the need to rebind when updating the RemoteVideoMenu - Allow popover to append and remove React Components from itself - Refactor popover so post-popover creation calls are broken out and popover removal behavior is all done in one function.
This commit is contained in:
parent
73dd7440d0
commit
da99f3b939
|
@ -1,5 +1,13 @@
|
||||||
/* global $ */
|
/* global $ */
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
|
||||||
|
import { i18next } from '../../../react/features/base/i18n';
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
const positionConfigurations = {
|
const positionConfigurations = {
|
||||||
left: {
|
left: {
|
||||||
|
|
||||||
|
@ -155,7 +163,7 @@ var JitsiPopover = (function () {
|
||||||
* Hides the popover and clears the document elements added by popover.
|
* Hides the popover and clears the document elements added by popover.
|
||||||
*/
|
*/
|
||||||
JitsiPopover.prototype.forceHide = function () {
|
JitsiPopover.prototype.forceHide = function () {
|
||||||
$(".jitsipopover").remove();
|
this.remove();
|
||||||
this.popoverShown = false;
|
this.popoverShown = false;
|
||||||
if(this.popoverIsHovered) { //the browser is not firing hover events
|
if(this.popoverIsHovered) { //the browser is not firing hover events
|
||||||
//when the element was on hover if got removed.
|
//when the element was on hover if got removed.
|
||||||
|
@ -170,21 +178,59 @@ var JitsiPopover = (function () {
|
||||||
JitsiPopover.prototype.createPopover = function () {
|
JitsiPopover.prototype.createPopover = function () {
|
||||||
$("body").append(this.template);
|
$("body").append(this.template);
|
||||||
let popoverElem = $(".jitsipopover > .jitsipopover__content");
|
let popoverElem = $(".jitsipopover > .jitsipopover__content");
|
||||||
popoverElem.html(this.options.content);
|
|
||||||
if(typeof this.options.onBeforePosition === "function") {
|
const { content } = this.options;
|
||||||
this.options.onBeforePosition($(".jitsipopover"));
|
|
||||||
|
if (React.isValidElement(content)) {
|
||||||
|
/* jshint ignore:start */
|
||||||
|
ReactDOM.render(
|
||||||
|
<I18nextProvider i18n = { i18next }>
|
||||||
|
{ content }
|
||||||
|
</I18nextProvider>,
|
||||||
|
popoverElem.get(0),
|
||||||
|
() => {
|
||||||
|
// FIXME There seems to be odd timing interaction when a
|
||||||
|
// React Component is manually removed from the DOM and then
|
||||||
|
// created again, as the ReactDOM callback will fire before
|
||||||
|
// render is called on the React Component. Using a timeout
|
||||||
|
// looks to bypass this behavior, maybe by creating
|
||||||
|
// different execution context. JitsiPopover should be
|
||||||
|
// rewritten into react soon anyway or at least rewritten
|
||||||
|
// so the html isn't completely torn down with each update.
|
||||||
|
setTimeout(() => this._popoverCreated());
|
||||||
|
});
|
||||||
|
/* jshint ignore:end */
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
var self = this;
|
|
||||||
$(".jitsipopover").on("mouseenter", function () {
|
popoverElem.html(content);
|
||||||
self.popoverIsHovered = true;
|
this._popoverCreated();
|
||||||
if(typeof self.onHoverPopover === "function") {
|
};
|
||||||
self.onHoverPopover(self.popoverIsHovered);
|
|
||||||
|
/**
|
||||||
|
* Adds listeners and executes callbacks after the popover has been created
|
||||||
|
* and displayed.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
JitsiPopover.prototype._popoverCreated = function () {
|
||||||
|
const { onBeforePosition } = this.options;
|
||||||
|
|
||||||
|
if (typeof onBeforePosition === 'function') {
|
||||||
|
onBeforePosition($(".jitsipopover"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.jitsipopover').on('mouseenter', () => {
|
||||||
|
this.popoverIsHovered = true;
|
||||||
|
if (typeof this.onHoverPopover === 'function') {
|
||||||
|
this.onHoverPopover(this.popoverIsHovered);
|
||||||
}
|
}
|
||||||
}).on("mouseleave", function () {
|
}).on('mouseleave', () => {
|
||||||
self.popoverIsHovered = false;
|
this.popoverIsHovered = false;
|
||||||
self.hide();
|
this.hide();
|
||||||
if(typeof self.onHoverPopover === "function") {
|
if (typeof this.onHoverPopover === 'function') {
|
||||||
self.onHoverPopover(self.popoverIsHovered);
|
this.onHoverPopover(this.popoverIsHovered);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -220,10 +266,30 @@ var JitsiPopover = (function () {
|
||||||
this.options.content = content;
|
this.options.content = content;
|
||||||
if(!this.popoverShown)
|
if(!this.popoverShown)
|
||||||
return;
|
return;
|
||||||
$(".jitsipopover").remove();
|
this.remove();
|
||||||
this.createPopover();
|
this.createPopover();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmounts any present child React Component and removes the popover itself
|
||||||
|
* from the DOM.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
JitsiPopover.prototype.remove = function () {
|
||||||
|
const $popover = $('.jitsipopover');
|
||||||
|
const $popoverContent = $popover.find('.jitsipopover__content');
|
||||||
|
const attachedComponent = $popoverContent.get(0);
|
||||||
|
|
||||||
|
if (attachedComponent) {
|
||||||
|
// ReactDOM will no-op if no React Component is found.
|
||||||
|
ReactDOM.unmountComponentAtNode(attachedComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
$popover.off();
|
||||||
|
$popover.remove();
|
||||||
|
};
|
||||||
|
|
||||||
JitsiPopover.enabled = true;
|
JitsiPopover.enabled = true;
|
||||||
|
|
||||||
return JitsiPopover;
|
return JitsiPopover;
|
||||||
|
|
|
@ -1,4 +1,18 @@
|
||||||
/* global $, APP, interfaceConfig, JitsiMeetJS */
|
/* global $, APP, interfaceConfig, JitsiMeetJS */
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MuteButton,
|
||||||
|
KickButton,
|
||||||
|
REMOTE_CONTROL_MENU_STATES,
|
||||||
|
RemoteControlButton,
|
||||||
|
RemoteVideoMenu,
|
||||||
|
VolumeSlider
|
||||||
|
} from '../../../react/features/remote-video-menu';
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||||
|
|
||||||
import ConnectionIndicator from './ConnectionIndicator';
|
import ConnectionIndicator from './ConnectionIndicator';
|
||||||
|
@ -58,6 +72,16 @@ function RemoteVideo(user, VideoLayout, emitter) {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.mutedWhileDisconnected = false;
|
this.mutedWhileDisconnected = false;
|
||||||
|
|
||||||
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
|
// TODO The event handlers should be turned into actions so changes can be
|
||||||
|
// handled through reducers and middleware.
|
||||||
|
this._kickHandler = this._kickHandler.bind(this);
|
||||||
|
this._muteHandler = this._muteHandler.bind(this);
|
||||||
|
this._requestRemoteControlPermissions
|
||||||
|
= this._requestRemoteControlPermissions.bind(this);
|
||||||
|
this._setAudioVolume = this._setAudioVolume.bind(this);
|
||||||
|
this._stopRemoteControl = this._stopRemoteControl.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
||||||
|
@ -133,92 +157,62 @@ RemoteVideo.prototype._isHovered = function () {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype._generatePopupContent = function () {
|
RemoteVideo.prototype._generatePopupContent = function () {
|
||||||
let popupmenuElement = document.createElement('ul');
|
const { controller } = APP.remoteControl;
|
||||||
popupmenuElement.className = 'popupmenu';
|
let remoteControlState = null;
|
||||||
popupmenuElement.id = `remote_popupmenu_${this.id}`;
|
let onRemoteControlToggle;
|
||||||
let menuItems = [];
|
|
||||||
|
|
||||||
if(APP.conference.isModerator) {
|
if (this._supportsRemoteControl) {
|
||||||
let muteTranslationKey;
|
if (controller.getRequestedParticipant() === this.id) {
|
||||||
let muteClassName;
|
onRemoteControlToggle = () => {};
|
||||||
if (this.isAudioMuted) {
|
remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
|
||||||
muteTranslationKey = 'videothumbnail.muted';
|
} else if (!controller.isStarted()) {
|
||||||
muteClassName = 'mutelink disabled';
|
onRemoteControlToggle = this._requestRemoteControlPermissions;
|
||||||
|
remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
|
||||||
} else {
|
} else {
|
||||||
muteTranslationKey = 'videothumbnail.domute';
|
onRemoteControlToggle = this._stopRemoteControl;
|
||||||
muteClassName = 'mutelink';
|
remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
let muteHandler = this._muteHandler.bind(this);
|
|
||||||
let kickHandler = this._kickHandler.bind(this);
|
|
||||||
|
|
||||||
menuItems = [
|
|
||||||
{
|
|
||||||
id: 'mutelink_' + this.id,
|
|
||||||
handler: muteHandler,
|
|
||||||
icon: 'icon-mic-disabled',
|
|
||||||
className: muteClassName,
|
|
||||||
data: {
|
|
||||||
i18n: muteTranslationKey
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
id: 'ejectlink_' + this.id,
|
|
||||||
handler: kickHandler,
|
|
||||||
icon: 'icon-kick',
|
|
||||||
data: {
|
|
||||||
i18n: 'videothumbnail.kick'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this._supportsRemoteControl) {
|
let initialVolumeValue, onVolumeChange;
|
||||||
let icon, handler, className;
|
|
||||||
if(APP.remoteControl.controller.getRequestedParticipant()
|
|
||||||
=== this.id) {
|
|
||||||
handler = () => {};
|
|
||||||
className = "requestRemoteControlLink disabled";
|
|
||||||
icon = "remote-control-spinner fa fa-spinner fa-spin";
|
|
||||||
} else if(!APP.remoteControl.controller.isStarted()) {
|
|
||||||
handler = this._requestRemoteControlPermissions.bind(this);
|
|
||||||
icon = "fa fa-play";
|
|
||||||
className = "requestRemoteControlLink";
|
|
||||||
} else {
|
|
||||||
handler = this._stopRemoteControl.bind(this);
|
|
||||||
icon = "fa fa-stop";
|
|
||||||
className = "requestRemoteControlLink";
|
|
||||||
}
|
|
||||||
menuItems.push({
|
|
||||||
id: 'remoteControl_' + this.id,
|
|
||||||
handler,
|
|
||||||
icon,
|
|
||||||
className,
|
|
||||||
data: {
|
|
||||||
i18n: 'videothumbnail.remoteControl'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
menuItems.forEach(el => {
|
// Feature check for volume setting as temasys objects cannot adjust volume.
|
||||||
let menuItem = this._generatePopupMenuItem(el);
|
|
||||||
popupmenuElement.appendChild(menuItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// feature check for volume setting as temasys objects cannot adjust volume
|
|
||||||
if (this._canSetAudioVolume()) {
|
if (this._canSetAudioVolume()) {
|
||||||
const volumeScale = 100;
|
initialVolumeValue = this._getAudioElement().volume;
|
||||||
const volumeSlider = this._generatePopupMenuSliderItem({
|
onVolumeChange = this._setAudioVolume;
|
||||||
handler: this._setAudioVolume.bind(this, volumeScale),
|
|
||||||
icon: 'icon-volume',
|
|
||||||
initialValue: this._getAudioElement().volume * volumeScale,
|
|
||||||
maxValue: volumeScale
|
|
||||||
});
|
|
||||||
popupmenuElement.appendChild(volumeSlider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
APP.translation.translateElement($(popupmenuElement));
|
const { isModerator } = APP.conference;
|
||||||
|
const participantID = this.id;
|
||||||
|
|
||||||
return popupmenuElement;
|
/* jshint ignore:start */
|
||||||
|
return (
|
||||||
|
<RemoteVideoMenu id = { participantID }>
|
||||||
|
{ isModerator
|
||||||
|
? <MuteButton
|
||||||
|
isAudioMuted = { this.isAudioMuted }
|
||||||
|
onClick = { this._muteHandler }
|
||||||
|
participantID = { participantID } />
|
||||||
|
: null }
|
||||||
|
{ isModerator
|
||||||
|
? <KickButton
|
||||||
|
onClick = { this._kickHandler }
|
||||||
|
participantID = { participantID } />
|
||||||
|
: null }
|
||||||
|
{ remoteControlState
|
||||||
|
? <RemoteControlButton
|
||||||
|
onClick = { onRemoteControlToggle }
|
||||||
|
participantID = { participantID }
|
||||||
|
remoteControlState = { remoteControlState } />
|
||||||
|
: null }
|
||||||
|
{ onVolumeChange
|
||||||
|
? <VolumeSlider
|
||||||
|
initialValue = { initialVolumeValue }
|
||||||
|
onChange = { onVolumeChange } />
|
||||||
|
: null }
|
||||||
|
</RemoteVideoMenu>
|
||||||
|
);
|
||||||
|
/* jshint ignore:end */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -312,92 +306,6 @@ RemoteVideo.prototype._kickHandler = function () {
|
||||||
this.popover.forceHide();
|
this.popover.forceHide();
|
||||||
};
|
};
|
||||||
|
|
||||||
RemoteVideo.prototype._generatePopupMenuItem = function (opts = {}) {
|
|
||||||
let {
|
|
||||||
id,
|
|
||||||
handler,
|
|
||||||
icon,
|
|
||||||
data,
|
|
||||||
className
|
|
||||||
} = opts;
|
|
||||||
|
|
||||||
handler = handler || $.noop;
|
|
||||||
|
|
||||||
let menuItem = document.createElement('li');
|
|
||||||
menuItem.className = 'popupmenu__item';
|
|
||||||
|
|
||||||
let linkItem = document.createElement('a');
|
|
||||||
linkItem.className = 'popupmenu__link';
|
|
||||||
|
|
||||||
if (className) {
|
|
||||||
linkItem.className += ` ${className}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (icon) {
|
|
||||||
let indicator = document.createElement('span');
|
|
||||||
indicator.className = 'popupmenu__icon';
|
|
||||||
indicator.innerHTML = `<i class="${icon}"></i>`;
|
|
||||||
linkItem.appendChild(indicator);
|
|
||||||
}
|
|
||||||
|
|
||||||
let textContent = document.createElement('span');
|
|
||||||
textContent.className = 'popupmenu__text';
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
let dataKeys = Object.keys(data);
|
|
||||||
dataKeys.forEach(key => {
|
|
||||||
textContent.dataset[key] = data[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
linkItem.appendChild(textContent);
|
|
||||||
linkItem.id = id;
|
|
||||||
|
|
||||||
linkItem.onclick = handler;
|
|
||||||
menuItem.appendChild(linkItem);
|
|
||||||
|
|
||||||
return menuItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a div element with a slider.
|
|
||||||
*
|
|
||||||
* @param {object} options - Configuration for the div's display and slider.
|
|
||||||
* @param {string} options.icon - The classname for the icon to display.
|
|
||||||
* @param {int} options.maxValue - The maximum value on the slider. The default
|
|
||||||
* value is 100.
|
|
||||||
* @param {int} options.initialValue - The value the slider should start at.
|
|
||||||
* The default value is 0.
|
|
||||||
* @param {function} options.handler - The callback for slider value changes.
|
|
||||||
* @returns {Element} A div element with a slider.
|
|
||||||
*/
|
|
||||||
RemoteVideo.prototype._generatePopupMenuSliderItem = function (options) {
|
|
||||||
const template = `<div class='popupmenu__contents'>
|
|
||||||
<span class='popupmenu__icon'>
|
|
||||||
<i class=${options.icon}></i>
|
|
||||||
</span>
|
|
||||||
<div class='popupmenu__slider_container'>
|
|
||||||
<input class='popupmenu__slider'
|
|
||||||
type='range'
|
|
||||||
min='0'
|
|
||||||
max=${options.maxValue || 100}
|
|
||||||
value=${options.initialValue || 0}>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const menuItem = document.createElement('li');
|
|
||||||
menuItem.className = 'popupmenu__item';
|
|
||||||
menuItem.innerHTML = template;
|
|
||||||
|
|
||||||
const slider = menuItem.getElementsByClassName('popupmenu__slider')[0];
|
|
||||||
slider.oninput = function () {
|
|
||||||
options.handler(Number(slider.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
return menuItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the remote participant's audio element.
|
* Get the remote participant's audio element.
|
||||||
*
|
*
|
||||||
|
@ -420,12 +328,11 @@ RemoteVideo.prototype._canSetAudioVolume = function () {
|
||||||
/**
|
/**
|
||||||
* Change the remote participant's volume level.
|
* Change the remote participant's volume level.
|
||||||
*
|
*
|
||||||
* @param {int} scale - The maximum value the slider can go to.
|
|
||||||
* @param {int} newVal - The value to set the slider to.
|
* @param {int} newVal - The value to set the slider to.
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype._setAudioVolume = function (scale, newVal) {
|
RemoteVideo.prototype._setAudioVolume = function (newVal) {
|
||||||
if (this._canSetAudioVolume()) {
|
if (this._canSetAudioVolume()) {
|
||||||
this._getAudioElement().volume = newVal / scale;
|
this._getAudioElement().volume = newVal;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a React {@link Component} which displays a button for kicking out
|
||||||
|
* a participant from the conference.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class KickButton extends Component {
|
||||||
|
/**
|
||||||
|
* {@code KickButton} component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* The callback to invoke when the component is clicked.
|
||||||
|
*/
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the participant linked to the onClick callback.
|
||||||
|
*/
|
||||||
|
participantID: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { onClick, participantID, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RemoteVideoMenuButton
|
||||||
|
buttonText = { t('videothumbnail.kick') }
|
||||||
|
iconClass = 'icon-kick'
|
||||||
|
id = { `ejectlink_${participantID}` }
|
||||||
|
onClick = { onClick } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(KickButton);
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a React {@link Component} which displays a button for audio muting
|
||||||
|
* a participant in the conference.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class MuteButton extends Component {
|
||||||
|
/**
|
||||||
|
* {@code MuteButton} component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* Whether or not the participant is currently audio muted.
|
||||||
|
*/
|
||||||
|
isAudioMuted: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The callback to invoke when the component is clicked.
|
||||||
|
*/
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the participant linked to the onClick callback.
|
||||||
|
*/
|
||||||
|
participantID: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { isAudioMuted, onClick, participantID, t } = this.props;
|
||||||
|
const muteConfig = isAudioMuted ? {
|
||||||
|
translationKey: 'videothumbnail.muted',
|
||||||
|
muteClassName: 'mutelink disabled'
|
||||||
|
} : {
|
||||||
|
translationKey: 'videothumbnail.domute',
|
||||||
|
muteClassName: 'mutelink'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RemoteVideoMenuButton
|
||||||
|
buttonText = { t(muteConfig.translationKey) }
|
||||||
|
displayClass = { muteConfig.muteClassName }
|
||||||
|
iconClass = 'icon-mic-disabled'
|
||||||
|
id = { `mutelink_${participantID}` }
|
||||||
|
onClick = { onClick } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(MuteButton);
|
|
@ -0,0 +1,99 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||||
|
|
||||||
|
// TODO: Move these enums into the store after further reactification of the
|
||||||
|
// non-react RemoteVideo component.
|
||||||
|
export const REMOTE_CONTROL_MENU_STATES = {
|
||||||
|
NOT_SUPPORTED: 0,
|
||||||
|
NOT_STARTED: 1,
|
||||||
|
REQUESTING: 2,
|
||||||
|
STARTED: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a React {@link Component} which displays a button showing the
|
||||||
|
* current state of remote control for a participant and can start or stop a
|
||||||
|
* remote control session.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class RemoteControlButton extends Component {
|
||||||
|
/**
|
||||||
|
* {@code RemoteControlButton} component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* The callback to invoke when the component is clicked.
|
||||||
|
*/
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the participant linked to the onClick callback.
|
||||||
|
*/
|
||||||
|
participantID: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current status of remote control. Should be a number listed in
|
||||||
|
* the enum REMOTE_CONTROL_MENU_STATES.
|
||||||
|
*/
|
||||||
|
remoteControlState: React.PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {null|ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
onClick,
|
||||||
|
participantID,
|
||||||
|
remoteControlState,
|
||||||
|
t
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let className, icon;
|
||||||
|
|
||||||
|
switch (remoteControlState) {
|
||||||
|
case REMOTE_CONTROL_MENU_STATES.NOT_STARTED:
|
||||||
|
className = 'requestRemoteControlLink';
|
||||||
|
icon = 'fa fa-play';
|
||||||
|
break;
|
||||||
|
case REMOTE_CONTROL_MENU_STATES.REQUESTING:
|
||||||
|
className = 'requestRemoteControlLink disabled';
|
||||||
|
icon = 'remote-control-spinner fa fa-spinner fa-spin';
|
||||||
|
break;
|
||||||
|
case REMOTE_CONTROL_MENU_STATES.STARTED:
|
||||||
|
className = 'requestRemoteControlLink';
|
||||||
|
icon = 'fa fa-stop';
|
||||||
|
break;
|
||||||
|
case REMOTE_CONTROL_MENU_STATES.NOT_SUPPORTED:
|
||||||
|
|
||||||
|
// Intentionally fall through.
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RemoteVideoMenuButton
|
||||||
|
buttonText = { t('videothumbnail.remoteControl') }
|
||||||
|
displayClass = { className }
|
||||||
|
iconClass = { icon }
|
||||||
|
id = { `remoteControl_${participantID}` }
|
||||||
|
onClick = { onClick } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(RemoteControlButton);
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React {@code Component} responsible for displaying other components as a menu
|
||||||
|
* for manipulating remote participant state.
|
||||||
|
*
|
||||||
|
* @extends {Component}
|
||||||
|
*/
|
||||||
|
export default class RemoteVideoMenu extends Component {
|
||||||
|
/**
|
||||||
|
* {@code RemoteVideoMenu}'s property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* The components to place as the body of the {@code RemoteVideoMenu}.
|
||||||
|
*/
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id attribute to be added to the component's DOM for retrieval
|
||||||
|
* when querying the DOM. Not used directly by the component.
|
||||||
|
*/
|
||||||
|
id: React.PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className = 'popupmenu'
|
||||||
|
id = { this.props.id }>
|
||||||
|
{ this.props.children }
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React {@code Component} for displaying an action in {@code RemoteVideoMenu}.
|
||||||
|
*
|
||||||
|
* @extends {Component}
|
||||||
|
*/
|
||||||
|
export default class RemoteVideoMenuButton extends Component {
|
||||||
|
/**
|
||||||
|
* {@code RemoteVideoMenuButton}'s property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* Text to display within the component that describes the onClick
|
||||||
|
* action.
|
||||||
|
*/
|
||||||
|
buttonText: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional CSS classes to add to the component.
|
||||||
|
*/
|
||||||
|
displayClass: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CSS classes for the icon that will display within the component.
|
||||||
|
*/
|
||||||
|
iconClass: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id attribute to be added to the component's DOM for retrieval
|
||||||
|
* when querying the DOM. Not used directly by the component.
|
||||||
|
*/
|
||||||
|
id: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to invoke when the component is clicked.
|
||||||
|
*/
|
||||||
|
onClick: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
buttonText,
|
||||||
|
displayClass,
|
||||||
|
iconClass,
|
||||||
|
id,
|
||||||
|
onClick
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const linkClassName = `popupmenu__link ${displayClass || ''}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className = 'popupmenu__item'>
|
||||||
|
<a
|
||||||
|
className = { linkClassName }
|
||||||
|
id = { id }
|
||||||
|
onClick = { onClick }>
|
||||||
|
<span className = 'popupmenu__icon'>
|
||||||
|
<i className = { iconClass } />
|
||||||
|
</span>
|
||||||
|
<span className = 'popupmenu__text'>
|
||||||
|
{ buttonText }
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to modify initialValue, which is expected to be a decimal value between
|
||||||
|
* 0 and 1, and converts it to a number representable by an input slider, which
|
||||||
|
* recognizes whole numbers.
|
||||||
|
*/
|
||||||
|
const VOLUME_SLIDER_SCALE = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a React {@link Component} which displays an input slider for
|
||||||
|
* adjusting the local volume of a remote participant.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class VolumeSlider extends Component {
|
||||||
|
/**
|
||||||
|
* {@code VolumeSlider} component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* The value of the audio slider should display at when the component
|
||||||
|
* first mounts. Changes will be stored in state. The value should be a
|
||||||
|
* number between 0 and 1.
|
||||||
|
*/
|
||||||
|
initialValue: React.PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The callback to invoke when the audio slider value changes.
|
||||||
|
*/
|
||||||
|
onChange: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code VolumeSlider} instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only properties with which the new
|
||||||
|
* instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
/**
|
||||||
|
* The volume of the participant's audio element. The value will
|
||||||
|
* be represented by a slider.
|
||||||
|
*
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
volumeLevel: (props.initialValue || 0) * VOLUME_SLIDER_SCALE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
|
this._onVolumeChange = this._onVolumeChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<li className = 'popupmenu__item'>
|
||||||
|
<div className = 'popupmenu__contents'>
|
||||||
|
<span className = 'popupmenu__icon'>
|
||||||
|
<i className = 'icon-volume' />
|
||||||
|
</span>
|
||||||
|
<div className = 'popupmenu__slider_container'>
|
||||||
|
<input
|
||||||
|
className = 'popupmenu__slider'
|
||||||
|
max = { VOLUME_SLIDER_SCALE }
|
||||||
|
min = { 0 }
|
||||||
|
onChange = { this._onVolumeChange }
|
||||||
|
type = 'range'
|
||||||
|
value = { this.state.volumeLevel } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the internal state of the volume level for the volume slider.
|
||||||
|
* Invokes the prop onVolumeChange to notify of volume changes.
|
||||||
|
*
|
||||||
|
* @param {Object} event - DOM Event for slider change.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onVolumeChange(event) {
|
||||||
|
const volumeLevel = event.currentTarget.value;
|
||||||
|
|
||||||
|
this.props.onChange(volumeLevel / VOLUME_SLIDER_SCALE);
|
||||||
|
this.setState({ volumeLevel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VolumeSlider;
|
|
@ -0,0 +1,8 @@
|
||||||
|
export { default as KickButton } from './KickButton';
|
||||||
|
export { default as MuteButton } from './MuteButton';
|
||||||
|
export {
|
||||||
|
REMOTE_CONTROL_MENU_STATES,
|
||||||
|
default as RemoteControlButton
|
||||||
|
} from './RemoteControlButton';
|
||||||
|
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
|
||||||
|
export { default as VolumeSlider } from './VolumeSlider';
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './components';
|
Loading…
Reference in New Issue