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 $ */
|
||||
|
||||
/* 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 = {
|
||||
left: {
|
||||
|
||||
|
@ -155,7 +163,7 @@ var JitsiPopover = (function () {
|
|||
* Hides the popover and clears the document elements added by popover.
|
||||
*/
|
||||
JitsiPopover.prototype.forceHide = function () {
|
||||
$(".jitsipopover").remove();
|
||||
this.remove();
|
||||
this.popoverShown = false;
|
||||
if(this.popoverIsHovered) { //the browser is not firing hover events
|
||||
//when the element was on hover if got removed.
|
||||
|
@ -170,21 +178,59 @@ var JitsiPopover = (function () {
|
|||
JitsiPopover.prototype.createPopover = function () {
|
||||
$("body").append(this.template);
|
||||
let popoverElem = $(".jitsipopover > .jitsipopover__content");
|
||||
popoverElem.html(this.options.content);
|
||||
if(typeof this.options.onBeforePosition === "function") {
|
||||
this.options.onBeforePosition($(".jitsipopover"));
|
||||
|
||||
const { content } = this.options;
|
||||
|
||||
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 () {
|
||||
self.popoverIsHovered = true;
|
||||
if(typeof self.onHoverPopover === "function") {
|
||||
self.onHoverPopover(self.popoverIsHovered);
|
||||
|
||||
popoverElem.html(content);
|
||||
this._popoverCreated();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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"));
|
||||
}
|
||||
}).on("mouseleave", function () {
|
||||
self.popoverIsHovered = false;
|
||||
self.hide();
|
||||
if(typeof self.onHoverPopover === "function") {
|
||||
self.onHoverPopover(self.popoverIsHovered);
|
||||
|
||||
$('.jitsipopover').on('mouseenter', () => {
|
||||
this.popoverIsHovered = true;
|
||||
if (typeof this.onHoverPopover === 'function') {
|
||||
this.onHoverPopover(this.popoverIsHovered);
|
||||
}
|
||||
}).on('mouseleave', () => {
|
||||
this.popoverIsHovered = false;
|
||||
this.hide();
|
||||
if (typeof this.onHoverPopover === 'function') {
|
||||
this.onHoverPopover(this.popoverIsHovered);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -220,10 +266,30 @@ var JitsiPopover = (function () {
|
|||
this.options.content = content;
|
||||
if(!this.popoverShown)
|
||||
return;
|
||||
$(".jitsipopover").remove();
|
||||
this.remove();
|
||||
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;
|
||||
|
||||
return JitsiPopover;
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
/* 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);
|
||||
|
||||
import ConnectionIndicator from './ConnectionIndicator';
|
||||
|
@ -58,6 +72,16 @@ function RemoteVideo(user, VideoLayout, emitter) {
|
|||
* @type {boolean}
|
||||
*/
|
||||
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);
|
||||
|
@ -133,92 +157,62 @@ RemoteVideo.prototype._isHovered = function () {
|
|||
* @private
|
||||
*/
|
||||
RemoteVideo.prototype._generatePopupContent = function () {
|
||||
let popupmenuElement = document.createElement('ul');
|
||||
popupmenuElement.className = 'popupmenu';
|
||||
popupmenuElement.id = `remote_popupmenu_${this.id}`;
|
||||
let menuItems = [];
|
||||
const { controller } = APP.remoteControl;
|
||||
let remoteControlState = null;
|
||||
let onRemoteControlToggle;
|
||||
|
||||
if(APP.conference.isModerator) {
|
||||
let muteTranslationKey;
|
||||
let muteClassName;
|
||||
if (this.isAudioMuted) {
|
||||
muteTranslationKey = 'videothumbnail.muted';
|
||||
muteClassName = 'mutelink disabled';
|
||||
if (this._supportsRemoteControl) {
|
||||
if (controller.getRequestedParticipant() === this.id) {
|
||||
onRemoteControlToggle = () => {};
|
||||
remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
|
||||
} else if (!controller.isStarted()) {
|
||||
onRemoteControlToggle = this._requestRemoteControlPermissions;
|
||||
remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
|
||||
} else {
|
||||
muteTranslationKey = 'videothumbnail.domute';
|
||||
muteClassName = 'mutelink';
|
||||
onRemoteControlToggle = this._stopRemoteControl;
|
||||
remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
let muteHandler = this._muteHandler.bind(this);
|
||||
let kickHandler = this._kickHandler.bind(this);
|
||||
let initialVolumeValue, onVolumeChange;
|
||||
|
||||
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 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 => {
|
||||
let menuItem = this._generatePopupMenuItem(el);
|
||||
popupmenuElement.appendChild(menuItem);
|
||||
});
|
||||
|
||||
// feature check for volume setting as temasys objects cannot adjust volume
|
||||
// Feature check for volume setting as temasys objects cannot adjust volume.
|
||||
if (this._canSetAudioVolume()) {
|
||||
const volumeScale = 100;
|
||||
const volumeSlider = this._generatePopupMenuSliderItem({
|
||||
handler: this._setAudioVolume.bind(this, volumeScale),
|
||||
icon: 'icon-volume',
|
||||
initialValue: this._getAudioElement().volume * volumeScale,
|
||||
maxValue: volumeScale
|
||||
});
|
||||
popupmenuElement.appendChild(volumeSlider);
|
||||
initialVolumeValue = this._getAudioElement().volume;
|
||||
onVolumeChange = this._setAudioVolume;
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -420,12 +328,11 @@ RemoteVideo.prototype._canSetAudioVolume = function () {
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
RemoteVideo.prototype._setAudioVolume = function (scale, newVal) {
|
||||
RemoteVideo.prototype._setAudioVolume = function (newVal) {
|
||||
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