diff --git a/modules/UI/util/JitsiPopover.js b/modules/UI/util/JitsiPopover.js
index ce6686b39..3bb226aa3 100644
--- a/modules/UI/util/JitsiPopover.js
+++ b/modules/UI/util/JitsiPopover.js
@@ -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(
+
+ { content }
+ ,
+ 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"));
+ }
+
+ $('.jitsipopover').on('mouseenter', () => {
+ this.popoverIsHovered = true;
+ if (typeof this.onHoverPopover === 'function') {
+ this.onHoverPopover(this.popoverIsHovered);
}
- }).on("mouseleave", function () {
- self.popoverIsHovered = false;
- self.hide();
- if(typeof self.onHoverPopover === "function") {
- self.onHoverPopover(self.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;
diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js
index b04d6fec6..9001cdb81 100644
--- a/modules/UI/videolayout/RemoteVideo.js
+++ b/modules/UI/videolayout/RemoteVideo.js
@@ -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);
-
- 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'
- }
- });
- }
+ let initialVolumeValue, onVolumeChange;
- 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 (
+
+ { isModerator
+ ?
+ : null }
+ { isModerator
+ ?
+ : null }
+ { remoteControlState
+ ?
+ : null }
+ { onVolumeChange
+ ?
+ : null }
+
+ );
+ /* 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 = ``;
- 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 = `
`;
-
- 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;
}
};
diff --git a/react/features/remote-video-menu/components/KickButton.js b/react/features/remote-video-menu/components/KickButton.js
new file mode 100644
index 000000000..7504c6a56
--- /dev/null
+++ b/react/features/remote-video-menu/components/KickButton.js
@@ -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 (
+
+ );
+ }
+}
+
+export default translate(KickButton);
diff --git a/react/features/remote-video-menu/components/MuteButton.js b/react/features/remote-video-menu/components/MuteButton.js
new file mode 100644
index 000000000..2f59e3040
--- /dev/null
+++ b/react/features/remote-video-menu/components/MuteButton.js
@@ -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 (
+
+ );
+ }
+}
+
+export default translate(MuteButton);
diff --git a/react/features/remote-video-menu/components/RemoteControlButton.js b/react/features/remote-video-menu/components/RemoteControlButton.js
new file mode 100644
index 000000000..77c8ec5b9
--- /dev/null
+++ b/react/features/remote-video-menu/components/RemoteControlButton.js
@@ -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 (
+
+ );
+ }
+}
+
+export default translate(RemoteControlButton);
diff --git a/react/features/remote-video-menu/components/RemoteVideoMenu.js b/react/features/remote-video-menu/components/RemoteVideoMenu.js
new file mode 100644
index 000000000..45a427025
--- /dev/null
+++ b/react/features/remote-video-menu/components/RemoteVideoMenu.js
@@ -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 (
+
+ { this.props.children }
+
+ );
+ }
+}
diff --git a/react/features/remote-video-menu/components/RemoteVideoMenuButton.js b/react/features/remote-video-menu/components/RemoteVideoMenuButton.js
new file mode 100644
index 000000000..8b1487502
--- /dev/null
+++ b/react/features/remote-video-menu/components/RemoteVideoMenuButton.js
@@ -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 (
+
+
+
+
+
+
+ { buttonText }
+
+
+
+ );
+ }
+}
diff --git a/react/features/remote-video-menu/components/VolumeSlider.js b/react/features/remote-video-menu/components/VolumeSlider.js
new file mode 100644
index 000000000..ae73794c5
--- /dev/null
+++ b/react/features/remote-video-menu/components/VolumeSlider.js
@@ -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 (
+
+
+
+ );
+ }
+
+ /**
+ * 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;
diff --git a/react/features/remote-video-menu/components/index.js b/react/features/remote-video-menu/components/index.js
new file mode 100644
index 000000000..214fda9e8
--- /dev/null
+++ b/react/features/remote-video-menu/components/index.js
@@ -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';
diff --git a/react/features/remote-video-menu/index.js b/react/features/remote-video-menu/index.js
new file mode 100644
index 000000000..07635cbbc
--- /dev/null
+++ b/react/features/remote-video-menu/index.js
@@ -0,0 +1 @@
+export * from './components';