Browse Source

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.
master
Leonard Kim 8 years ago
parent
commit
da99f3b939

+ 81
- 15
modules/UI/util/JitsiPopover.js View File

@@ -1,5 +1,13 @@
1 1
 /* global $ */
2 2
 
3
+/* eslint-disable no-unused-vars */
4
+import React, { Component } from 'react';
5
+import ReactDOM from 'react-dom';
6
+import { I18nextProvider } from 'react-i18next';
7
+
8
+import { i18next } from '../../../react/features/base/i18n';
9
+/* eslint-enable no-unused-vars */
10
+
3 11
 const positionConfigurations = {
4 12
     left: {
5 13
 
@@ -155,7 +163,7 @@ var JitsiPopover = (function () {
155 163
      * Hides the popover and clears the document elements added by popover.
156 164
      */
157 165
     JitsiPopover.prototype.forceHide = function () {
158
-        $(".jitsipopover").remove();
166
+        this.remove();
159 167
         this.popoverShown = false;
160 168
         if(this.popoverIsHovered) { //the browser is not firing hover events
161 169
             //when the element was on hover if got removed.
@@ -170,21 +178,59 @@ var JitsiPopover = (function () {
170 178
     JitsiPopover.prototype.createPopover = function () {
171 179
         $("body").append(this.template);
172 180
         let popoverElem = $(".jitsipopover > .jitsipopover__content");
173
-        popoverElem.html(this.options.content);
174
-        if(typeof this.options.onBeforePosition === "function") {
175
-            this.options.onBeforePosition($(".jitsipopover"));
181
+
182
+        const { content } = this.options;
183
+
184
+        if (React.isValidElement(content)) {
185
+            /* jshint ignore:start */
186
+            ReactDOM.render(
187
+                <I18nextProvider i18n = { i18next }>
188
+                    { content }
189
+                </I18nextProvider>,
190
+                popoverElem.get(0),
191
+                () => {
192
+                    // FIXME There seems to be odd timing interaction when a
193
+                    // React Component is manually removed from the DOM and then
194
+                    // created again, as the ReactDOM callback will fire before
195
+                    // render is called on the React Component. Using a timeout
196
+                    // looks to bypass this behavior, maybe by creating
197
+                    // different execution context. JitsiPopover should be
198
+                    // rewritten into react soon anyway or at least rewritten
199
+                    // so the html isn't completely torn down with each update.
200
+                    setTimeout(() => this._popoverCreated());
201
+                });
202
+            /* jshint ignore:end */
203
+            return;
176 204
         }
177
-        var self = this;
178
-        $(".jitsipopover").on("mouseenter", function () {
179
-            self.popoverIsHovered = true;
180
-            if(typeof self.onHoverPopover === "function") {
181
-                self.onHoverPopover(self.popoverIsHovered);
205
+
206
+        popoverElem.html(content);
207
+        this._popoverCreated();
208
+    };
209
+
210
+    /**
211
+     * Adds listeners and executes callbacks after the popover has been created
212
+     * and displayed.
213
+     *
214
+     * @private
215
+     * @returns {void}
216
+     */
217
+    JitsiPopover.prototype._popoverCreated = function () {
218
+        const { onBeforePosition } = this.options;
219
+
220
+        if (typeof onBeforePosition === 'function') {
221
+            onBeforePosition($(".jitsipopover"));
222
+        }
223
+
224
+        $('.jitsipopover').on('mouseenter', () => {
225
+            this.popoverIsHovered = true;
226
+            if (typeof this.onHoverPopover === 'function') {
227
+                this.onHoverPopover(this.popoverIsHovered);
182 228
             }
183
-        }).on("mouseleave", function () {
184
-            self.popoverIsHovered = false;
185
-            self.hide();
186
-            if(typeof self.onHoverPopover === "function") {
187
-                self.onHoverPopover(self.popoverIsHovered);
229
+        }).on('mouseleave', () => {
230
+            this.popoverIsHovered = false;
231
+            this.hide();
232
+            if (typeof this.onHoverPopover === 'function') {
233
+                this.onHoverPopover(this.popoverIsHovered);
188 234
             }
189 235
         });
190 236
 
@@ -220,10 +266,30 @@ var JitsiPopover = (function () {
220 266
         this.options.content = content;
221 267
         if(!this.popoverShown)
222 268
             return;
223
-        $(".jitsipopover").remove();
269
+        this.remove();
224 270
         this.createPopover();
225 271
     };
226 272
 
273
+    /**
274
+     * Unmounts any present child React Component and removes the popover itself
275
+     * from the DOM.
276
+     *
277
+     * @returns {void}
278
+     */
279
+    JitsiPopover.prototype.remove = function () {
280
+        const $popover = $('.jitsipopover');
281
+        const $popoverContent = $popover.find('.jitsipopover__content');
282
+        const attachedComponent = $popoverContent.get(0);
283
+
284
+        if (attachedComponent) {
285
+            // ReactDOM will no-op if no React Component is found.
286
+            ReactDOM.unmountComponentAtNode(attachedComponent);
287
+        }
288
+
289
+        $popover.off();
290
+        $popover.remove();
291
+    };
292
+
227 293
     JitsiPopover.enabled = true;
228 294
 
229 295
     return JitsiPopover;

+ 74
- 167
modules/UI/videolayout/RemoteVideo.js View File

@@ -1,4 +1,18 @@
1 1
 /* global $, APP, interfaceConfig, JitsiMeetJS */
2
+
3
+/* eslint-disable no-unused-vars */
4
+import React from 'react';
5
+
6
+import {
7
+    MuteButton,
8
+    KickButton,
9
+    REMOTE_CONTROL_MENU_STATES,
10
+    RemoteControlButton,
11
+    RemoteVideoMenu,
12
+    VolumeSlider
13
+} from '../../../react/features/remote-video-menu';
14
+/* eslint-enable no-unused-vars */
15
+
2 16
 const logger = require("jitsi-meet-logger").getLogger(__filename);
3 17
 
4 18
 import ConnectionIndicator from './ConnectionIndicator';
@@ -58,6 +72,16 @@ function RemoteVideo(user, VideoLayout, emitter) {
58 72
      * @type {boolean}
59 73
      */
60 74
     this.mutedWhileDisconnected = false;
75
+
76
+    // Bind event handlers so they are only bound once for every instance.
77
+    // TODO The event handlers should be turned into actions so changes can be
78
+    // handled through reducers and middleware.
79
+    this._kickHandler = this._kickHandler.bind(this);
80
+    this._muteHandler = this._muteHandler.bind(this);
81
+    this._requestRemoteControlPermissions
82
+        = this._requestRemoteControlPermissions.bind(this);
83
+    this._setAudioVolume = this._setAudioVolume.bind(this);
84
+    this._stopRemoteControl = this._stopRemoteControl.bind(this);
61 85
 }
62 86
 
63 87
 RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@@ -133,92 +157,62 @@ RemoteVideo.prototype._isHovered = function () {
133 157
  * @private
134 158
  */
135 159
 RemoteVideo.prototype._generatePopupContent = function () {
136
-    let popupmenuElement = document.createElement('ul');
137
-    popupmenuElement.className = 'popupmenu';
138
-    popupmenuElement.id = `remote_popupmenu_${this.id}`;
139
-    let menuItems = [];
140
-
141
-    if(APP.conference.isModerator) {
142
-        let muteTranslationKey;
143
-        let muteClassName;
144
-        if (this.isAudioMuted) {
145
-            muteTranslationKey = 'videothumbnail.muted';
146
-            muteClassName = 'mutelink disabled';
160
+    const { controller } = APP.remoteControl;
161
+    let remoteControlState = null;
162
+    let onRemoteControlToggle;
163
+
164
+    if (this._supportsRemoteControl) {
165
+        if (controller.getRequestedParticipant() === this.id) {
166
+            onRemoteControlToggle = () => {};
167
+            remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
168
+        } else if (!controller.isStarted()) {
169
+            onRemoteControlToggle = this._requestRemoteControlPermissions;
170
+            remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
147 171
         } else {
148
-            muteTranslationKey = 'videothumbnail.domute';
149
-            muteClassName = 'mutelink';
172
+            onRemoteControlToggle = this._stopRemoteControl;
173
+            remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
150 174
         }
151
-
152
-        let muteHandler = this._muteHandler.bind(this);
153
-        let kickHandler = this._kickHandler.bind(this);
154
-
155
-        menuItems = [
156
-            {
157
-                id: 'mutelink_' + this.id,
158
-                handler: muteHandler,
159
-                icon: 'icon-mic-disabled',
160
-                className: muteClassName,
161
-                data: {
162
-                    i18n: muteTranslationKey
163
-                }
164
-            }, {
165
-                id: 'ejectlink_' + this.id,
166
-                handler: kickHandler,
167
-                icon: 'icon-kick',
168
-                data: {
169
-                    i18n: 'videothumbnail.kick'
170
-                }
171
-            }
172
-        ];
173 175
     }
174 176
 
175
-    if(this._supportsRemoteControl) {
176
-        let icon, handler, className;
177
-        if(APP.remoteControl.controller.getRequestedParticipant()
178
-            === this.id) {
179
-            handler = () => {};
180
-            className = "requestRemoteControlLink disabled";
181
-            icon = "remote-control-spinner fa fa-spinner fa-spin";
182
-        } else if(!APP.remoteControl.controller.isStarted()) {
183
-            handler = this._requestRemoteControlPermissions.bind(this);
184
-            icon = "fa fa-play";
185
-            className = "requestRemoteControlLink";
186
-        } else {
187
-            handler = this._stopRemoteControl.bind(this);
188
-            icon = "fa fa-stop";
189
-            className = "requestRemoteControlLink";
190
-        }
191
-        menuItems.push({
192
-            id: 'remoteControl_' + this.id,
193
-            handler,
194
-            icon,
195
-            className,
196
-            data: {
197
-                i18n: 'videothumbnail.remoteControl'
198
-            }
199
-        });
200
-    }
201
-
202
-    menuItems.forEach(el => {
203
-        let menuItem = this._generatePopupMenuItem(el);
204
-        popupmenuElement.appendChild(menuItem);
205
-    });
177
+    let initialVolumeValue, onVolumeChange;
206 178
 
207
-    // feature check for volume setting as temasys objects cannot adjust volume
179
+    // Feature check for volume setting as temasys objects cannot adjust volume.
208 180
     if (this._canSetAudioVolume()) {
209
-        const volumeScale = 100;
210
-        const volumeSlider = this._generatePopupMenuSliderItem({
211
-            handler: this._setAudioVolume.bind(this, volumeScale),
212
-            icon: 'icon-volume',
213
-            initialValue: this._getAudioElement().volume * volumeScale,
214
-            maxValue: volumeScale
215
-        });
216
-        popupmenuElement.appendChild(volumeSlider);
181
+        initialVolumeValue = this._getAudioElement().volume;
182
+        onVolumeChange = this._setAudioVolume;
217 183
     }
218 184
 
219
-    APP.translation.translateElement($(popupmenuElement));
220
-
221
-    return popupmenuElement;
185
+    const { isModerator } = APP.conference;
186
+    const participantID = this.id;
187
+
188
+    /* jshint ignore:start */
189
+    return (
190
+        <RemoteVideoMenu id = { participantID }>
191
+            { isModerator
192
+                ? <MuteButton
193
+                    isAudioMuted = { this.isAudioMuted }
194
+                    onClick = { this._muteHandler }
195
+                    participantID = { participantID } />
196
+                : null }
197
+            { isModerator
198
+                ? <KickButton
199
+                    onClick = { this._kickHandler }
200
+                    participantID = { participantID } />
201
+                : null }
202
+            { remoteControlState
203
+                ? <RemoteControlButton
204
+                    onClick = { onRemoteControlToggle }
205
+                    participantID = { participantID }
206
+                    remoteControlState = { remoteControlState } />
207
+                : null }
208
+            { onVolumeChange
209
+                ? <VolumeSlider
210
+                    initialValue = { initialVolumeValue }
211
+                    onChange = { onVolumeChange } />
212
+                : null }
213
+        </RemoteVideoMenu>
214
+    );
215
+    /* jshint ignore:end */
222 216
 };
223 217
 
224 218
 /**
@@ -312,92 +306,6 @@ RemoteVideo.prototype._kickHandler = function () {
312 306
     this.popover.forceHide();
313 307
 };
314 308
 
315
-RemoteVideo.prototype._generatePopupMenuItem = function (opts = {}) {
316
-    let {
317
-        id,
318
-        handler,
319
-        icon,
320
-        data,
321
-        className
322
-    } = opts;
323
-
324
-    handler = handler || $.noop;
325
-
326
-    let menuItem = document.createElement('li');
327
-    menuItem.className = 'popupmenu__item';
328
-
329
-    let linkItem = document.createElement('a');
330
-    linkItem.className = 'popupmenu__link';
331
-
332
-    if (className) {
333
-        linkItem.className += ` ${className}`;
334
-    }
335
-
336
-    if (icon) {
337
-        let indicator = document.createElement('span');
338
-        indicator.className = 'popupmenu__icon';
339
-        indicator.innerHTML = `<i class="${icon}"></i>`;
340
-        linkItem.appendChild(indicator);
341
-    }
342
-
343
-    let textContent = document.createElement('span');
344
-    textContent.className = 'popupmenu__text';
345
-
346
-    if (data) {
347
-        let dataKeys = Object.keys(data);
348
-        dataKeys.forEach(key => {
349
-            textContent.dataset[key] = data[key];
350
-        });
351
-    }
352
-
353
-    linkItem.appendChild(textContent);
354
-    linkItem.id = id;
355
-
356
-    linkItem.onclick = handler;
357
-    menuItem.appendChild(linkItem);
358
-
359
-    return menuItem;
360
-};
361
-
362
-/**
363
- * Create a div element with a slider.
364
- *
365
- * @param {object} options - Configuration for the div's display and slider.
366
- * @param {string} options.icon - The classname for the icon to display.
367
- * @param {int} options.maxValue - The maximum value on the slider. The default
368
- * value is 100.
369
- * @param {int} options.initialValue - The value the slider should start at.
370
- * The default value is 0.
371
- * @param {function} options.handler - The callback for slider value changes.
372
- * @returns {Element} A div element with a slider.
373
- */
374
-RemoteVideo.prototype._generatePopupMenuSliderItem = function (options) {
375
-    const template = `<div class='popupmenu__contents'>
376
-        <span class='popupmenu__icon'>
377
-            <i class=${options.icon}></i>
378
-        </span>
379
-        <div class='popupmenu__slider_container'>
380
-            <input class='popupmenu__slider'
381
-                type='range'
382
-                min='0'
383
-                max=${options.maxValue || 100}
384
-                value=${options.initialValue || 0}>
385
-            </input>
386
-        </div>
387
-    </div>`;
388
-
389
-    const menuItem = document.createElement('li');
390
-    menuItem.className = 'popupmenu__item';
391
-    menuItem.innerHTML = template;
392
-
393
-    const slider = menuItem.getElementsByClassName('popupmenu__slider')[0];
394
-    slider.oninput = function () {
395
-        options.handler(Number(slider.value));
396
-    };
397
-
398
-    return menuItem;
399
-};
400
-
401 309
 /**
402 310
  * Get the remote participant's audio element.
403 311
  *
@@ -420,12 +328,11 @@ RemoteVideo.prototype._canSetAudioVolume = function () {
420 328
 /**
421 329
  * Change the remote participant's volume level.
422 330
  *
423
- * @param {int} scale - The maximum value the slider can go to.
424 331
  * @param {int} newVal - The value to set the slider to.
425 332
  */
426
-RemoteVideo.prototype._setAudioVolume = function (scale, newVal) {
333
+RemoteVideo.prototype._setAudioVolume = function (newVal) {
427 334
     if (this._canSetAudioVolume()) {
428
-        this._getAudioElement().volume = newVal / scale;
335
+        this._getAudioElement().volume = newVal;
429 336
     }
430 337
 };
431 338
 

+ 55
- 0
react/features/remote-video-menu/components/KickButton.js View File

@@ -0,0 +1,55 @@
1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
6
+
7
+/**
8
+ * Implements a React {@link Component} which displays a button for kicking out
9
+ * a participant from the conference.
10
+ *
11
+ * @extends Component
12
+ */
13
+class KickButton extends Component {
14
+    /**
15
+     * {@code KickButton} component's property types.
16
+     *
17
+     * @static
18
+     */
19
+    static propTypes = {
20
+        /**
21
+         * The callback to invoke when the component is clicked.
22
+         */
23
+        onClick: React.PropTypes.func,
24
+
25
+        /**
26
+         * The ID of the participant linked to the onClick callback.
27
+         */
28
+        participantID: React.PropTypes.string,
29
+
30
+        /**
31
+         * Invoked to obtain translated strings.
32
+         */
33
+        t: React.PropTypes.func
34
+    };
35
+
36
+    /**
37
+     * Implements React's {@link Component#render()}.
38
+     *
39
+     * @inheritdoc
40
+     * @returns {ReactElement}
41
+     */
42
+    render() {
43
+        const { onClick, participantID, t } = this.props;
44
+
45
+        return (
46
+            <RemoteVideoMenuButton
47
+                buttonText = { t('videothumbnail.kick') }
48
+                iconClass = 'icon-kick'
49
+                id = { `ejectlink_${participantID}` }
50
+                onClick = { onClick } />
51
+        );
52
+    }
53
+}
54
+
55
+export default translate(KickButton);

+ 68
- 0
react/features/remote-video-menu/components/MuteButton.js View File

@@ -0,0 +1,68 @@
1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
6
+
7
+/**
8
+ * Implements a React {@link Component} which displays a button for audio muting
9
+ * a participant in the conference.
10
+ *
11
+ * @extends Component
12
+ */
13
+class MuteButton extends Component {
14
+    /**
15
+     * {@code MuteButton} component's property types.
16
+     *
17
+     * @static
18
+     */
19
+    static propTypes = {
20
+        /**
21
+         * Whether or not the participant is currently audio muted.
22
+         */
23
+        isAudioMuted: React.PropTypes.bool,
24
+
25
+        /**
26
+         * The callback to invoke when the component is clicked.
27
+         */
28
+        onClick: React.PropTypes.func,
29
+
30
+        /**
31
+         * The ID of the participant linked to the onClick callback.
32
+         */
33
+        participantID: React.PropTypes.string,
34
+
35
+        /**
36
+         * Invoked to obtain translated strings.
37
+         */
38
+        t: React.PropTypes.func
39
+    };
40
+
41
+    /**
42
+     * Implements React's {@link Component#render()}.
43
+     *
44
+     * @inheritdoc
45
+     * @returns {ReactElement}
46
+     */
47
+    render() {
48
+        const { isAudioMuted, onClick, participantID, t } = this.props;
49
+        const muteConfig = isAudioMuted ? {
50
+            translationKey: 'videothumbnail.muted',
51
+            muteClassName: 'mutelink disabled'
52
+        } : {
53
+            translationKey: 'videothumbnail.domute',
54
+            muteClassName: 'mutelink'
55
+        };
56
+
57
+        return (
58
+            <RemoteVideoMenuButton
59
+                buttonText = { t(muteConfig.translationKey) }
60
+                displayClass = { muteConfig.muteClassName }
61
+                iconClass = 'icon-mic-disabled'
62
+                id = { `mutelink_${participantID}` }
63
+                onClick = { onClick } />
64
+        );
65
+    }
66
+}
67
+
68
+export default translate(MuteButton);

+ 99
- 0
react/features/remote-video-menu/components/RemoteControlButton.js View File

@@ -0,0 +1,99 @@
1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
6
+
7
+// TODO: Move these enums into the store after further reactification of the
8
+// non-react RemoteVideo component.
9
+export const REMOTE_CONTROL_MENU_STATES = {
10
+    NOT_SUPPORTED: 0,
11
+    NOT_STARTED: 1,
12
+    REQUESTING: 2,
13
+    STARTED: 3
14
+};
15
+
16
+/**
17
+ * Implements a React {@link Component} which displays a button showing the
18
+ * current state of remote control for a participant and can start or stop a
19
+ * remote control session.
20
+ *
21
+ * @extends Component
22
+ */
23
+class RemoteControlButton extends Component {
24
+    /**
25
+     * {@code RemoteControlButton} component's property types.
26
+     *
27
+     * @static
28
+     */
29
+    static propTypes = {
30
+        /**
31
+         * The callback to invoke when the component is clicked.
32
+         */
33
+        onClick: React.PropTypes.func,
34
+
35
+        /**
36
+         * The ID of the participant linked to the onClick callback.
37
+         */
38
+        participantID: React.PropTypes.string,
39
+
40
+        /**
41
+         * The current status of remote control. Should be a number listed in
42
+         * the enum REMOTE_CONTROL_MENU_STATES.
43
+         */
44
+        remoteControlState: React.PropTypes.number,
45
+
46
+        /**
47
+         * Invoked to obtain translated strings.
48
+         */
49
+        t: React.PropTypes.func
50
+    };
51
+
52
+    /**
53
+     * Implements React's {@link Component#render()}.
54
+     *
55
+     * @inheritdoc
56
+     * @returns {null|ReactElement}
57
+     */
58
+    render() {
59
+        const {
60
+            onClick,
61
+            participantID,
62
+            remoteControlState,
63
+            t
64
+        } = this.props;
65
+
66
+        let className, icon;
67
+
68
+        switch (remoteControlState) {
69
+        case REMOTE_CONTROL_MENU_STATES.NOT_STARTED:
70
+            className = 'requestRemoteControlLink';
71
+            icon = 'fa fa-play';
72
+            break;
73
+        case REMOTE_CONTROL_MENU_STATES.REQUESTING:
74
+            className = 'requestRemoteControlLink disabled';
75
+            icon = 'remote-control-spinner fa fa-spinner fa-spin';
76
+            break;
77
+        case REMOTE_CONTROL_MENU_STATES.STARTED:
78
+            className = 'requestRemoteControlLink';
79
+            icon = 'fa fa-stop';
80
+            break;
81
+        case REMOTE_CONTROL_MENU_STATES.NOT_SUPPORTED:
82
+
83
+            // Intentionally fall through.
84
+        default:
85
+            return null;
86
+        }
87
+
88
+        return (
89
+            <RemoteVideoMenuButton
90
+                buttonText = { t('videothumbnail.remoteControl') }
91
+                displayClass = { className }
92
+                iconClass = { icon }
93
+                id = { `remoteControl_${participantID}` }
94
+                onClick = { onClick } />
95
+        );
96
+    }
97
+}
98
+
99
+export default translate(RemoteControlButton);

+ 43
- 0
react/features/remote-video-menu/components/RemoteVideoMenu.js View File

@@ -0,0 +1,43 @@
1
+import React, { Component } from 'react';
2
+
3
+/**
4
+ * React {@code Component} responsible for displaying other components as a menu
5
+ * for manipulating remote participant state.
6
+ *
7
+ * @extends {Component}
8
+ */
9
+export default class RemoteVideoMenu extends Component {
10
+    /**
11
+     * {@code RemoteVideoMenu}'s property types.
12
+     *
13
+     * @static
14
+     */
15
+    static propTypes = {
16
+        /**
17
+         * The components to place as the body of the {@code RemoteVideoMenu}.
18
+         */
19
+        children: React.PropTypes.node,
20
+
21
+        /**
22
+         * The id attribute to be added to the component's DOM for retrieval
23
+         * when querying the DOM. Not used directly by the component.
24
+         */
25
+        id: React.PropTypes.string
26
+    };
27
+
28
+    /**
29
+     * Implements React's {@link Component#render()}.
30
+     *
31
+     * @inheritdoc
32
+     * @returns {ReactElement}
33
+     */
34
+    render() {
35
+        return (
36
+            <ul
37
+                className = 'popupmenu'
38
+                id = { this.props.id }>
39
+                { this.props.children }
40
+            </ul>
41
+        );
42
+    }
43
+}

+ 76
- 0
react/features/remote-video-menu/components/RemoteVideoMenuButton.js View File

@@ -0,0 +1,76 @@
1
+import React, { Component } from 'react';
2
+
3
+/**
4
+ * React {@code Component} for displaying an action in {@code RemoteVideoMenu}.
5
+ *
6
+ * @extends {Component}
7
+ */
8
+export default class RemoteVideoMenuButton extends Component {
9
+    /**
10
+     * {@code RemoteVideoMenuButton}'s property types.
11
+     *
12
+     * @static
13
+     */
14
+    static propTypes = {
15
+        /**
16
+         * Text to display within the component that describes the onClick
17
+         * action.
18
+         */
19
+        buttonText: React.PropTypes.string,
20
+
21
+        /**
22
+         * Additional CSS classes to add to the component.
23
+         */
24
+        displayClass: React.PropTypes.string,
25
+
26
+        /**
27
+         * The CSS classes for the icon that will display within the component.
28
+         */
29
+        iconClass: React.PropTypes.string,
30
+
31
+        /**
32
+         * The id attribute to be added to the component's DOM for retrieval
33
+         * when querying the DOM. Not used directly by the component.
34
+         */
35
+        id: React.PropTypes.string,
36
+
37
+        /**
38
+         * Callback to invoke when the component is clicked.
39
+         */
40
+        onClick: React.PropTypes.func
41
+    };
42
+
43
+    /**
44
+     * Implements React's {@link Component#render()}.
45
+     *
46
+     * @inheritdoc
47
+     * @returns {ReactElement}
48
+     */
49
+    render() {
50
+        const {
51
+            buttonText,
52
+            displayClass,
53
+            iconClass,
54
+            id,
55
+            onClick
56
+        } = this.props;
57
+
58
+        const linkClassName = `popupmenu__link ${displayClass || ''}`;
59
+
60
+        return (
61
+            <li className = 'popupmenu__item'>
62
+                <a
63
+                    className = { linkClassName }
64
+                    id = { id }
65
+                    onClick = { onClick }>
66
+                    <span className = 'popupmenu__icon'>
67
+                        <i className = { iconClass } />
68
+                    </span>
69
+                    <span className = 'popupmenu__text'>
70
+                        { buttonText }
71
+                    </span>
72
+                </a>
73
+            </li>
74
+        );
75
+    }
76
+}

+ 102
- 0
react/features/remote-video-menu/components/VolumeSlider.js View File

@@ -0,0 +1,102 @@
1
+import React, { Component } from 'react';
2
+
3
+/**
4
+ * Used to modify initialValue, which is expected to be a decimal value between
5
+ * 0 and 1, and converts it to a number representable by an input slider, which
6
+ * recognizes whole numbers.
7
+ */
8
+const VOLUME_SLIDER_SCALE = 100;
9
+
10
+/**
11
+ * Implements a React {@link Component} which displays an input slider for
12
+ * adjusting the local volume of a remote participant.
13
+ *
14
+ * @extends Component
15
+ */
16
+class VolumeSlider extends Component {
17
+    /**
18
+     * {@code VolumeSlider} component's property types.
19
+     *
20
+     * @static
21
+     */
22
+    static propTypes = {
23
+        /**
24
+         * The value of the audio slider should display at when the component
25
+         * first mounts. Changes will be stored in state. The value should be a
26
+         * number between 0 and 1.
27
+         */
28
+        initialValue: React.PropTypes.number,
29
+
30
+        /**
31
+         * The callback to invoke when the audio slider value changes.
32
+         */
33
+        onChange: React.PropTypes.func
34
+    };
35
+
36
+    /**
37
+     * Initializes a new {@code VolumeSlider} instance.
38
+     *
39
+     * @param {Object} props - The read-only properties with which the new
40
+     * instance is to be initialized.
41
+     */
42
+    constructor(props) {
43
+        super(props);
44
+
45
+        this.state = {
46
+            /**
47
+             * The volume of the participant's audio element. The value will
48
+             * be represented by a slider.
49
+             *
50
+             * @type {Number}
51
+             */
52
+            volumeLevel: (props.initialValue || 0) * VOLUME_SLIDER_SCALE
53
+        };
54
+
55
+        // Bind event handlers so they are only bound once for every instance.
56
+        this._onVolumeChange = this._onVolumeChange.bind(this);
57
+    }
58
+
59
+    /**
60
+     * Implements React's {@link Component#render()}.
61
+     *
62
+     * @inheritdoc
63
+     * @returns {ReactElement}
64
+     */
65
+    render() {
66
+        return (
67
+            <li className = 'popupmenu__item'>
68
+                <div className = 'popupmenu__contents'>
69
+                    <span className = 'popupmenu__icon'>
70
+                        <i className = 'icon-volume' />
71
+                    </span>
72
+                    <div className = 'popupmenu__slider_container'>
73
+                        <input
74
+                            className = 'popupmenu__slider'
75
+                            max = { VOLUME_SLIDER_SCALE }
76
+                            min = { 0 }
77
+                            onChange = { this._onVolumeChange }
78
+                            type = 'range'
79
+                            value = { this.state.volumeLevel } />
80
+                    </div>
81
+                </div>
82
+            </li>
83
+        );
84
+    }
85
+
86
+    /**
87
+     * Sets the internal state of the volume level for the volume slider.
88
+     * Invokes the prop onVolumeChange to notify of volume changes.
89
+     *
90
+     * @param {Object} event - DOM Event for slider change.
91
+     * @private
92
+     * @returns {void}
93
+     */
94
+    _onVolumeChange(event) {
95
+        const volumeLevel = event.currentTarget.value;
96
+
97
+        this.props.onChange(volumeLevel / VOLUME_SLIDER_SCALE);
98
+        this.setState({ volumeLevel });
99
+    }
100
+}
101
+
102
+export default VolumeSlider;

+ 8
- 0
react/features/remote-video-menu/components/index.js View File

@@ -0,0 +1,8 @@
1
+export { default as KickButton } from './KickButton';
2
+export { default as MuteButton } from './MuteButton';
3
+export {
4
+    REMOTE_CONTROL_MENU_STATES,
5
+    default as RemoteControlButton
6
+} from './RemoteControlButton';
7
+export { default as RemoteVideoMenu } from './RemoteVideoMenu';
8
+export { default as VolumeSlider } from './VolumeSlider';

+ 1
- 0
react/features/remote-video-menu/index.js View File

@@ -0,0 +1 @@
1
+export * from './components';

Loading…
Cancel
Save