瀏覽代碼

feat(jitsipopover): convert to InlineDialog (#1804)

* feat(small-video): use InlineDialog for stats and remote menu

- Remove JitsiPopover and use InlineDialog instead.
- Bring the remote menu icon into react.
- Make vertical filmstrip position:fixed so popper (AtlasKit
  dependency) sets InlineDialogs and eventually tooltips to
  position:fixed.

* ref(remote-menu): hook KickButton to redux

* ref(remote-menu): hook MuteButton to redux

* modify padding, toggle dialogs

* pixel push margins to align dialogs, adjust padding of dialogs

* add comment about margin for dialog, add file I forgot

* modify indicator markup so the icon can be moved down while trigger stays at top of toolbar
master
virtuacoplenny 7 年之前
父節點
當前提交
725d39ddcd

+ 14
- 5
css/_connection-info.scss 查看文件

@@ -1,8 +1,7 @@
1 1
 %connection-info {
2
-    text-align: left;
3 2
     font-size: 12px;
4 3
     font-weight: 400;
5
-    color: $popoverFontColor;
4
+    color: $modalTextColor;
6 5
 
7 6
     td {
8 7
         padding: 2px 0;
@@ -11,11 +10,14 @@
11 10
 
12 11
 .connection-info
13 12
 {
14
-    float: left;
15
-    padding: 5px;
16
-    padding-left: 0;
17 13
     @extend %connection-info;
18 14
 
15
+    /**
16
+     * Apply negative margin to reduce the appearance of padding in AtlasKit
17
+     * InlineDialog.
18
+     */
19
+    margin: -15px;
20
+
19 21
     > table {
20 22
         white-space: nowrap;
21 23
         @extend %connection-info;
@@ -40,4 +42,11 @@
40 42
         @extend .connection-info__icon;
41 43
         color: $uploadConnectionIconColor;
42 44
     }
45
+
46
+    .showmore {
47
+        display: block;
48
+        margin: 10px auto;
49
+        text-align: center;
50
+        width: 90px;
51
+    }
43 52
 }

+ 0
- 92
css/_jitsi_popover.scss 查看文件

@@ -1,92 +0,0 @@
1
-.jitsipopover {
2
-    position: absolute;
3
-    top: 0;
4
-    left: 0;
5
-    z-index: $jitsipopoverZ;
6
-    display: table;
7
-    visibility: hidden;
8
-    max-width: 300px;
9
-    min-width: 100px;
10
-    text-align: left;
11
-    color: $popoverFontColor;
12
-    background-color: $popoverBg;
13
-    background-clip: padding-box;
14
-    border-radius: $borderRadius;
15
-    /*-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);*/
16
-    /*box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);*/
17
-    white-space: normal;
18
-    margin-top: -$popoverMenuPadding;
19
-
20
-
21
-    &__menu-padding,
22
-    &__menu-padding-top {
23
-        position: absolute;
24
-        width: 100px;
25
-    }
26
-
27
-    /**
28
-     * Invisible padding is added to the bottom of the popover to extend its
29
-     * height so it does not close when moving the mouse from the trigger
30
-     * element towards the popover itself.
31
-     */
32
-    &__menu-padding {
33
-        bottom: -$popoverMenuPadding;
34
-        height: $popoverMenuPadding;
35
-    }
36
-
37
-    /**
38
-     * Invisible padding is added to the top of the popover to extend its height
39
-     * so it does not close automatically when its height is shrunk from showing
40
-     * less video statistics.
41
-     */
42
-    &__menu-padding-top {
43
-        height: 20px;
44
-        top: -20px;
45
-    }
46
-
47
-    &__showmore {
48
-        display: block;
49
-        text-align: center;
50
-        width: 90px;
51
-        margin: 10px auto;
52
-    }
53
-
54
-    > .arrow {
55
-        position: absolute;
56
-        display: block;
57
-        left: 50%;
58
-        bottom: -5px;
59
-        margin-left: -5px;
60
-        width: 0;
61
-        height: 0;
62
-        border-color: transparent;
63
-        border-top-color: $popoverBg;
64
-        border-style: solid;
65
-        border-width: 5px;
66
-        border-bottom-width: 0;
67
-    }
68
-
69
-    /**
70
-     * Override default "top" styles to support popovers appearing from the
71
-     * left of the popover trigger element.
72
-     */
73
-    &.left {
74
-        margin-left: -$popoverMenuPadding;
75
-        margin-top: 0;
76
-
77
-        .arrow {
78
-            border-color: transparent transparent transparent $popoverBg;
79
-            border-width: 5px 0px 5px 5px;
80
-            margin-left: 0;
81
-            margin-top: -5px;
82
-        }
83
-
84
-        .jitsipopover {
85
-            &__menu-padding {
86
-                bottom: 0;
87
-                height: 100%;
88
-                width: $popoverMenuPadding;
89
-            }
90
-        }
91
-    }
92
-}

+ 21
- 13
css/_popup_menu.scss 查看文件

@@ -3,37 +3,31 @@
3 3
 **/
4 4
 
5 5
 .popupmenu {
6
+    text-align: left;
6 7
     padding: 0;
7
-    margin: 2px 0;
8
-    bottom: 0;
9
-    height: auto;
10
-
11
-    &:first-child {
12
-        margin-top: 2px;
13
-    }
8
+    white-space: nowrap;
14 9
 
15 10
     &__item {
16 11
         list-style-type: none;
17
-        text-align: left;
18 12
         height: 35px;
19 13
 
20 14
         &:hover {
21
-            background-color: $popupMenuSelectedItemBackground;
15
+            background-color: rgba(9, 30, 66, 0.04);
22 16
         }
23 17
     }
24 18
 
25 19
     // Link Appearance
26 20
     &__link,
27 21
     &__contents {
22
+        color: $modalTextColor;
28 23
         display: block;
29 24
         box-sizing: border-box;
30 25
         text-decoration: none;
31
-        color: #fff;
32
-        padding: 5px;
33 26
         height: 100%;
34 27
         font-size: 9pt;
35 28
         width: 100%;
36
-        cursor: hand;
29
+        cursor: pointer;
30
+        padding: 0 5px;
37 31
 
38 32
         &.disabled {
39 33
             color: gray !important;
@@ -46,6 +40,12 @@
46 40
         vertical-align: middle;
47 41
     }
48 42
 
43
+    &__link {
44
+        i {
45
+            cursor: pointer;
46
+        }
47
+    }
48
+
49 49
     &__contents {
50 50
         display: flex;
51 51
 
@@ -73,7 +73,6 @@
73 73
         display: inline-block;
74 74
         min-width: 20px;
75 75
         height: 100%;
76
-        text-align: center;
77 76
 
78 77
         > * {
79 78
             @include absoluteAligning();
@@ -85,6 +84,15 @@
85 84
     }
86 85
 }
87 86
 
87
+/**
88
+ * Override reset css styling modifying all lists and set negative margin to
89
+ * reduce the visibility of padding on AtlasKit
90
+ * InlineDialogs.
91
+ */
92
+ul.popupmenu {
93
+    margin: -15px;
94
+}
95
+
88 96
 span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
89 97
     display:block !important;
90 98
 }

+ 0
- 1
css/_variables.scss 查看文件

@@ -112,7 +112,6 @@ $tooltipsZ: 401;
112 112
 $dropdownMaskZ: 900;
113 113
 $dropdownZ: 901;
114 114
 $centeredVideoLabelZ: 1010;
115
-$jitsipopoverZ: 1012;
116 115
 $popoverZ: 1015;
117 116
 $overlayZ: 1016;
118 117
 

+ 26
- 7
css/_vertical_filmstrip_overrides.scss 查看文件

@@ -8,6 +8,14 @@
8 8
         display: flex;
9 9
         flex-direction: column-reverse;
10 10
         height: 100%;
11
+        /**
12
+         * fixed positioning is necessary for remote menus and tooltips to pop
13
+         * out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
14
+         * a library called popper which will position its elements fixed if
15
+         * any parent is also fixed.
16
+         */
17
+        position: fixed;
18
+        z-index: $tooltipsZ;
11 19
 
12 20
         /**
13 21
          * Hide videos by making them slight to the right.
@@ -60,13 +68,16 @@
60 68
          * Move the remote video menu trigger to the bottom left of the
61 69
          * video thumbnail.
62 70
          */
63
-        .remotevideomenu {
71
+        .remotevideomenu,
72
+        .remote-video-menu-trigger {
64 73
             bottom: 0;
65 74
             left: 0;
66 75
             top: auto;
67 76
             right: auto;
77
+        }
78
+
79
+        .remote-video-menu-trigger {
68 80
             margin-bottom: 7px;
69
-            transform: translate3d(0,0,0);
70 81
         }
71 82
 
72 83
         #remoteVideos {
@@ -75,11 +86,6 @@
75 86
         }
76 87
 
77 88
         .videocontainer {
78
-            &__toolbar,
79
-            &__toptoolbar {
80
-                transform: translate3d(0,0,0);
81
-            }
82
-
83 89
             /**
84 90
              * Move status icons to the bottom right of the thumbnail.
85 91
              */
@@ -159,4 +165,17 @@
159 165
             transition-delay: 0.1s;
160 166
         }
161 167
     }
168
+
169
+    /**
170
+     * Apply hardware acceleration to prevent flickering on scroll. The
171
+     * selectors are specific to icon wrappers to prevent fixed position dialogs
172
+     * and tooltips from getting a new location context due to translate3d.
173
+     */
174
+    .connection-indicator,
175
+    .remote-video-menu-trigger,
176
+    .videocontainer__toolbar,
177
+    .raisehandindicator,
178
+    #dominantspeakerindicator {
179
+        transform: translate3d(0, 0, 0);
180
+    }
162 181
 }

+ 32
- 5
css/_videolayout_default.scss 查看文件

@@ -48,15 +48,30 @@
48 48
 
49 49
     &__toolbar {
50 50
         bottom: 0;
51
-        padding: 0 5px 0 5px;
52 51
         height: $thumbnailToolbarHeight;
52
+        padding: 0 5px 0 5px;
53 53
     }
54 54
 
55 55
     &__toptoolbar {
56
-        $toolbarPadding: 5px;
56
+        $toolbarIconMargin: 5px;
57 57
         top: 0;
58
-        padding: $toolbarPadding;
59 58
         padding-bottom: 0;
59
+        /**
60
+         * Override text-align center as icons need to be left justified.
61
+         */
62
+        text-align: left;
63
+
64
+        /**
65
+         * Intentionally use margin on the icon itself as AtlasKit InlineDialog
66
+         * positioning depends on the trigger (indicator icon).
67
+         */
68
+        .indicator {
69
+            margin-top: $toolbarIconMargin;
70
+        }
71
+
72
+        .indicator:nth-child(1) {
73
+            margin-left: $toolbarIconMargin;
74
+        }
60 75
 
61 76
         .connection-indicator,
62 77
         span.indicator {
@@ -71,6 +86,15 @@
71 86
             }
72 87
         }
73 88
 
89
+        .connection-indicator-container {
90
+            display: inline-block;
91
+            vertical-align: top;
92
+
93
+            .popover-trigger {
94
+                display: inline-block;
95
+            }
96
+        }
97
+
74 98
         .connection-indicator,
75 99
         span.indicator {
76 100
             position: relative;
@@ -78,7 +102,6 @@
78 102
             text-align: center;
79 103
             line-height: $thumbnailIndicatorSize;
80 104
             padding: 0;
81
-            float: left;
82 105
             @include circle($thumbnailIndicatorSize);
83 106
             box-sizing: border-box;
84 107
             z-index: $zindex3;
@@ -124,6 +147,7 @@
124 147
 
125 148
             .icon-connection,
126 149
             .icon-connection-lost {
150
+                cursor: pointer;
127 151
                 font-size: 1em;
128 152
             }
129 153
         }
@@ -309,13 +333,13 @@
309 333
   background: $connectionIndicatorBg;
310 334
 }
311 335
 
336
+.remote-video-menu-trigger,
312 337
 .remotevideomenu
313 338
 {
314 339
     display: inline-block;
315 340
     position: absolute;
316 341
     top: 0px;
317 342
     right: 0;
318
-    margin-top: 7px;
319 343
     z-index: $zindex3;
320 344
     width: 18px;
321 345
     height: 13px;
@@ -326,6 +350,9 @@
326 350
         cursor: hand;
327 351
     }
328 352
 }
353
+.remote-video-menu-trigger {
354
+    margin-top: 7px;
355
+}
329 356
 
330 357
 /**
331 358
  * Audio indicator on video thumbnails.

+ 0
- 1
css/main.scss 查看文件

@@ -50,7 +50,6 @@
50 50
 @import 'recording';
51 51
 @import 'login_menu';
52 52
 @import 'popover';
53
-@import 'jitsi_popover';
54 53
 @import 'contact_list';
55 54
 @import 'chat';
56 55
 @import 'ringing/ringing';

+ 0
- 2
modules/UI/UI.js 查看文件

@@ -8,7 +8,6 @@ import Chat from "./side_pannels/chat/Chat";
8 8
 import SidePanels from "./side_pannels/SidePanels";
9 9
 import Avatar from "./avatar/Avatar";
10 10
 import SideContainerToggler from "./side_pannels/SideContainerToggler";
11
-import JitsiPopover from "./util/JitsiPopover";
12 11
 import messageHandler from "./util/MessageHandler";
13 12
 import UIUtil from "./util/UIUtil";
14 13
 import { activateTooltips } from './util/Tooltip';
@@ -322,7 +321,6 @@ UI.start = function () {
322 321
         UI.showToolbar();
323 322
         Filmstrip.setFilmstripOnly();
324 323
         APP.store.dispatch(setNotificationsEnabled(false));
325
-        JitsiPopover.enabled = false;
326 324
     }
327 325
 
328 326
     if (interfaceConfig.VERTICAL_FILMSTRIP) {

+ 0
- 276
modules/UI/util/JitsiPopover.js 查看文件

@@ -1,276 +0,0 @@
1
-/* global $ */
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
-
11
-const positionConfigurations = {
12
-    left: {
13
-
14
-        // Align the popover's right side to the target element.
15
-        my: 'right',
16
-
17
-        // Align the popover to the left side of the target element.
18
-        at: 'left',
19
-
20
-        // Force the popover to fit within the viewport.
21
-        collision: 'fit',
22
-
23
-        /**
24
-         * Callback invoked by jQuery UI tooltip.
25
-         *
26
-         * @param {Object} position - The top and bottom position the popover
27
-         * element should be set at.
28
-         * @param {Object} element. - Additional size and position information
29
-         * about the popover element and target.
30
-         * @param {Object} elements.element - Has position and size related data
31
-         * for the popover element itself.
32
-         * @param {Object} elements.target - Has position and size related data
33
-         * for the target element the popover displays from.
34
-         */
35
-        using: function setPositionLeft(position, elements) {
36
-            const { element, target } = elements;
37
-
38
-            $('.jitsipopover').css({
39
-                left: element.left,
40
-                top: element.top,
41
-                visibility: 'visible'
42
-            });
43
-
44
-            // Move additional padding to the right edge of the popover and
45
-            // allow css to take care of width. The padding is used to maintain
46
-            // a hover state between the target and the popover.
47
-            $('.jitsipopover > .jitsipopover__menu-padding')
48
-                .css({ left: element.width });
49
-
50
-            // Find the distance from the top of the popover to the center of
51
-            // the target and use that value to position the arrow to point to
52
-            // it.
53
-            const verticalCenterOfTarget = target.height / 2;
54
-            const verticalDistanceFromTops = target.top - element.top;
55
-            const verticalPositionOfTargetCenter
56
-                = verticalDistanceFromTops + verticalCenterOfTarget;
57
-
58
-            $('.jitsipopover > .arrow').css({
59
-                left: element.width,
60
-                top: verticalPositionOfTargetCenter
61
-            });
62
-        }
63
-    },
64
-    top: {
65
-        my: "bottom",
66
-        at: "top",
67
-        collision: "fit",
68
-        using: function setPositionTop(position, elements) {
69
-            const { element, target } = elements;
70
-            const calcLeft = target.left - element.left + target.width / 2;
71
-            const paddingLeftPosition = calcLeft - 50;
72
-            const $jistiPopover = $('.jitsipopover');
73
-
74
-            $jistiPopover.css({
75
-                left: element.left,
76
-                top: element.top,
77
-                visibility: 'visible'
78
-            });
79
-            $jistiPopover.find('.arrow').css({ left: calcLeft });
80
-            $jistiPopover.find('.jitsipopover__menu-padding')
81
-                .css({ left: paddingLeftPosition });
82
-            $jistiPopover.find('.jitsipopover__menu-padding-top')
83
-                .css({ left: paddingLeftPosition });
84
-        }
85
-    }
86
-};
87
-export default (function () {
88
-    /**
89
-     * The default options
90
-     */
91
-    const defaultOptions = {
92
-        skin: 'white',
93
-        content: '',
94
-        hasArrow: true,
95
-        onBeforePosition: undefined,
96
-        position: 'top'
97
-    };
98
-
99
-    /**
100
-     * Constructs new JitsiPopover and attaches it to the element
101
-     * @param element jquery selector
102
-     * @param options the options for the popover.
103
-     * @constructor
104
-     */
105
-    function JitsiPopover(element, options)
106
-    {
107
-        this.options = Object.assign({}, defaultOptions, options);
108
-        this.elementIsHovered = false;
109
-        this.popoverIsHovered = false;
110
-        this.popoverShown = false;
111
-
112
-        element.data("jitsi_popover", this);
113
-        this.element = element;
114
-        this.template = this.getTemplate();
115
-        var self = this;
116
-        this.element.on("mouseenter", function () {
117
-            self.elementIsHovered = true;
118
-            self.show();
119
-        }).on("mouseleave", function () {
120
-            self.elementIsHovered = false;
121
-            setTimeout(function () {
122
-                self.hide();
123
-            }, 10);
124
-        });
125
-    }
126
-
127
-    /**
128
-     * Returns template for popover
129
-     */
130
-    JitsiPopover.prototype.getTemplate = function () {
131
-        const { hasArrow, position, skin } = this.options;
132
-
133
-        let arrow = '';
134
-        if (hasArrow) {
135
-            arrow = '<div class="arrow"></div>';
136
-        }
137
-
138
-        return  (
139
-            `<div class="jitsipopover ${skin} ${position}">
140
-                <div class="jitsipopover__menu-padding-top"></div>
141
-                ${arrow}
142
-                <div class="jitsipopover__content"></div>
143
-                <div class="jitsipopover__menu-padding"></div>
144
-            </div>`
145
-        );
146
-    };
147
-
148
-    /**
149
-     * Shows the popover
150
-     */
151
-    JitsiPopover.prototype.show = function () {
152
-        if(!JitsiPopover.enabled)
153
-            return;
154
-        this.createPopover();
155
-        this.popoverShown = true;
156
-    };
157
-
158
-    /**
159
-     * Hides the popover if not hovered or popover is not shown.
160
-     */
161
-    JitsiPopover.prototype.hide = function () {
162
-        if(!this.elementIsHovered && !this.popoverIsHovered &&
163
-            this.popoverShown) {
164
-            this.forceHide();
165
-        }
166
-    };
167
-
168
-    /**
169
-     * Hides the popover and clears the document elements added by popover.
170
-     */
171
-    JitsiPopover.prototype.forceHide = function () {
172
-        this.remove();
173
-        this.popoverShown = false;
174
-        if(this.popoverIsHovered) { //the browser is not firing hover events
175
-            //when the element was on hover if got removed.
176
-            this.popoverIsHovered = false;
177
-            this.onHoverPopover(this.popoverIsHovered);
178
-        }
179
-    };
180
-
181
-    /**
182
-     * Creates the popover html.
183
-     */
184
-    JitsiPopover.prototype.createPopover = function () {
185
-        let $popover = $('.jitsipopover');
186
-
187
-        if (!$popover.length) {
188
-            $('body').append(this.template);
189
-
190
-            $popover = $('.jitsipopover');
191
-
192
-            $popover.on('mouseenter', () => {
193
-                this.popoverIsHovered = true;
194
-                if (typeof this.onHoverPopover === 'function') {
195
-                    this.onHoverPopover(this.popoverIsHovered);
196
-                }
197
-            });
198
-
199
-            $popover.on('mouseleave', () => {
200
-                this.popoverIsHovered = false;
201
-                this.hide();
202
-                if (typeof this.onHoverPopover === 'function') {
203
-                    this.onHoverPopover(this.popoverIsHovered);
204
-                }
205
-            });
206
-        }
207
-
208
-        const $popoverContent = $popover.find('.jitsipopover__content');
209
-
210
-        /* jshint ignore:start */
211
-        ReactDOM.render(
212
-            <I18nextProvider i18n = { i18next }>
213
-                { this.options.content }
214
-            </I18nextProvider>,
215
-            $popoverContent.get(0),
216
-            () => {
217
-                this.refreshPosition();
218
-            });
219
-        /* jshint ignore:end */
220
-    };
221
-
222
-    /**
223
-     * Adds a hover listener to the popover.
224
-     */
225
-    JitsiPopover.prototype.addOnHoverPopover = function (listener) {
226
-        this.onHoverPopover = listener;
227
-    };
228
-
229
-    /**
230
-     * Refreshes the position of the popover.
231
-     */
232
-    JitsiPopover.prototype.refreshPosition = function () {
233
-        const positionOptions = Object.assign(
234
-            {},
235
-            positionConfigurations[this.options.position],
236
-            {
237
-                of: this.element
238
-            }
239
-        );
240
-        $(".jitsipopover").position(positionOptions);
241
-    };
242
-
243
-    /**
244
-     * Updates the content of popover.
245
-     * @param content new content
246
-     */
247
-    JitsiPopover.prototype.updateContent = function (content) {
248
-        this.options.content = content;
249
-        if (!this.popoverShown) {
250
-            return;
251
-        }
252
-        this.createPopover();
253
-    };
254
-
255
-    /**
256
-     * Unmounts any present child React Component and removes the popover itself
257
-     * from the DOM.
258
-     *
259
-     * @returns {void}
260
-     */
261
-    JitsiPopover.prototype.remove = function () {
262
-        const $popover = $('.jitsipopover');
263
-        const $popoverContent = $popover.find('.jitsipopover__content');
264
-
265
-        if ($popoverContent.length) {
266
-            ReactDOM.unmountComponentAtNode($popoverContent.get(0));
267
-        }
268
-
269
-        $popover.off();
270
-        $popover.remove();
271
-    };
272
-
273
-    JitsiPopover.enabled = true;
274
-
275
-    return JitsiPopover;
276
-})();

+ 19
- 5
modules/UI/videolayout/LocalVideo.js 查看文件

@@ -24,6 +24,9 @@ function LocalVideo(VideoLayout, emitter) {
24 24
         this._buildContextMenu();
25 25
     this.isLocal = true;
26 26
     this.emitter = emitter;
27
+    this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
28
+        ? 'left bottom' : 'top center';
29
+
27 30
     Object.defineProperty(this, 'id', {
28 31
         get: function () {
29 32
             return APP.conference.getMyUserId();
@@ -67,23 +70,34 @@ LocalVideo.prototype.changeVideo = function (stream) {
67 70
     this.videoStream = stream;
68 71
 
69 72
     let localVideoClick = (event) => {
70
-        // TODO Checking the classList is a workround to allow events to bubble
73
+        // TODO Checking the classes is a workround to allow events to bubble
71 74
         // into the DisplayName component if it was clicked. React's synthetic
72 75
         // events will fire after jQuery handlers execute, so stop propogation
73 76
         // at this point will prevent DisplayName from getting click events.
74 77
         // This workaround should be removeable once LocalVideo is a React
75 78
         // Component because then the components share the same eventing system.
79
+        const $source = $(event.target || event.srcElement);
76 80
         const { classList } = event.target;
77
-        const clickedOnDisplayName = classList.contains('displayname')
78
-            || classList.contains('editdisplayname');
81
+
82
+        const clickedOnDisplayName
83
+            = $source.parents('.displayNameContainer').length > 0;
84
+        const clickedOnPopover
85
+            = $source.parents('.connection-info').length > 0;
86
+        const clickedOnPopoverTrigger
87
+            = $source.parents('.popover-trigger').length > 0
88
+                || classList.contains('popover-trigger');
89
+
90
+        const ignoreClick = clickedOnDisplayName
91
+            || clickedOnPopoverTrigger
92
+            || clickedOnPopover;
79 93
 
80 94
         // FIXME: with Temasys plugin event arg is not an event, but
81 95
         // the clicked object itself, so we have to skip this call
82
-        if (event.stopPropagation && !clickedOnDisplayName) {
96
+        if (event.stopPropagation && !ignoreClick) {
83 97
             event.stopPropagation();
84 98
         }
85 99
 
86
-        if (!clickedOnDisplayName) {
100
+        if (!ignoreClick) {
87 101
             this.VideoLayout.handleVideoThumbClicked(this.id);
88 102
         }
89 103
     };

+ 61
- 137
modules/UI/videolayout/RemoteVideo.js 查看文件

@@ -4,15 +4,14 @@
4 4
 import React from 'react';
5 5
 import ReactDOM from 'react-dom';
6 6
 import { Provider } from 'react-redux';
7
+import { I18nextProvider } from 'react-i18next';
8
+
9
+import { i18next } from '../../../react/features/base/i18n';
7 10
 
8 11
 import { PresenceLabel } from '../../../react/features/presence-status';
9 12
 import {
10
-    MuteButton,
11
-    KickButton,
12 13
     REMOTE_CONTROL_MENU_STATES,
13
-    RemoteControlButton,
14
-    RemoteVideoMenu,
15
-    VolumeSlider
14
+    RemoteVideoMenuTriggerButton
16 15
 } from '../../../react/features/remote-video-menu';
17 16
 /* eslint-enable no-unused-vars */
18 17
 
@@ -21,13 +20,7 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
21 20
 
22 21
 import SmallVideo from "./SmallVideo";
23 22
 import UIUtils from "../util/UIUtil";
24
-import UIEvents from '../../../service/UI/UIEvents';
25
-import JitsiPopover from "../util/JitsiPopover";
26 23
 
27
-const MUTED_DIALOG_BUTTON_VALUES = {
28
-    cancel: 0,
29
-    muted: 1
30
-};
31 24
 const ParticipantConnectionStatus
32 25
     = JitsiMeetJS.constants.participantConnectionStatus;
33 26
 
@@ -49,6 +42,8 @@ function RemoteVideo(user, VideoLayout, emitter) {
49 42
     this._audioStreamElement = null;
50 43
     this.hasRemoteVideoMenu = false;
51 44
     this._supportsRemoteControl = false;
45
+    this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
46
+        ? 'left top' : 'top center';
52 47
     this.addRemoteVideoContainer();
53 48
     this.updateIndicators();
54 49
     this.setDisplayName();
@@ -78,8 +73,6 @@ function RemoteVideo(user, VideoLayout, emitter) {
78 73
     // Bind event handlers so they are only bound once for every instance.
79 74
     // TODO The event handlers should be turned into actions so changes can be
80 75
     // handled through reducers and middleware.
81
-    this._kickHandler = this._kickHandler.bind(this);
82
-    this._muteHandler = this._muteHandler.bind(this);
83 76
     this._requestRemoteControlPermissions
84 77
         = this._requestRemoteControlPermissions.bind(this);
85 78
     this._setAudioVolume = this._setAudioVolume.bind(this);
@@ -107,39 +100,6 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
107 100
     return this.container;
108 101
 };
109 102
 
110
-/**
111
- * Initializes the remote participant popup menu, by specifying previously
112
- * constructed popupMenuElement, containing all the menu items.
113
- *
114
- * @param popupMenuElement a pre-constructed element, containing the menu items
115
- * to display in the popup
116
- */
117
-RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
118
-    let options = {
119
-        content: popupMenuElement.outerHTML,
120
-        skin: "black",
121
-        hasArrow: false,
122
-        position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
123
-    };
124
-    let element = $("#" + this.videoSpanId + " .remotevideomenu");
125
-    this.popover = new JitsiPopover(element, options);
126
-    this.popover.addOnHoverPopover(isHovered => {
127
-        this.popupMenuIsHovered = isHovered;
128
-        this.updateView();
129
-    });
130
-
131
-    // override popover show method to make sure we will update the content
132
-    // before showing the popover
133
-    let origShowFunc = this.popover.show;
134
-    this.popover.show = function () {
135
-        // update content by forcing it, to finish even if popover
136
-        // is not visible
137
-        this.updateRemoteVideoMenu(this.isAudioMuted, true);
138
-        // call the original show, passing its actual this
139
-        origShowFunc.call(this.popover);
140
-    }.bind(this);
141
-};
142
-
143 103
 /**
144 104
  * Checks whether current video is considered hovered. Currently it is hovered
145 105
  * if the mouse is over the video, or if the connection indicator or the popup
@@ -160,6 +120,17 @@ RemoteVideo.prototype._isHovered = function () {
160 120
  * @private
161 121
  */
162 122
 RemoteVideo.prototype._generatePopupContent = function () {
123
+    if (interfaceConfig.filmStripOnly) {
124
+        return;
125
+    }
126
+
127
+    const remoteVideoMenuContainer
128
+        = this.container.querySelector('.remotevideomenu');
129
+
130
+    if (!remoteVideoMenuContainer) {
131
+        return;
132
+    }
133
+
163 134
     const { controller } = APP.remoteControl;
164 135
     let remoteControlState = null;
165 136
     let onRemoteControlToggle;
@@ -189,35 +160,28 @@ RemoteVideo.prototype._generatePopupContent = function () {
189 160
     const participantID = this.id;
190 161
 
191 162
     /* jshint ignore:start */
192
-    return (
193
-        <RemoteVideoMenu id = { participantID }>
194
-            { isModerator
195
-                ? <MuteButton
163
+    ReactDOM.render(
164
+        <Provider store = { APP.store }>
165
+            <I18nextProvider i18n = { i18next }>
166
+                <RemoteVideoMenuTriggerButton
167
+                    initialVolumeValue = { initialVolumeValue }
196 168
                     isAudioMuted = { this.isAudioMuted }
197
-                    onClick = { this._muteHandler }
198
-                    participantID = { participantID } />
199
-                : null }
200
-            { isModerator
201
-                ? <KickButton
202
-                    onClick = { this._kickHandler }
203
-                    participantID = { participantID } />
204
-                : null }
205
-            { remoteControlState
206
-                ? <RemoteControlButton
207
-                    onClick = { onRemoteControlToggle }
169
+                    isModerator = { isModerator }
170
+                    onMenuDisplay = { this._onRemoteVideoMenuDisplay.bind(this) }
171
+                    onRemoteControlToggle = { onRemoteControlToggle }
172
+                    onVolumeChange = { onVolumeChange }
208 173
                     participantID = { participantID }
209 174
                     remoteControlState = { remoteControlState } />
210
-                : null }
211
-            { onVolumeChange
212
-                ? <VolumeSlider
213
-                    initialValue = { initialVolumeValue }
214
-                    onChange = { onVolumeChange } />
215
-                : null }
216
-        </RemoteVideoMenu>
217
-    );
175
+            </I18nextProvider>
176
+        </Provider>,
177
+        remoteVideoMenuContainer);
218 178
     /* jshint ignore:end */
219 179
 };
220 180
 
181
+RemoteVideo.prototype._onRemoteVideoMenuDisplay = function () {
182
+    this.updateRemoteVideoMenu(this.isAudioMuted, true);
183
+};
184
+
221 185
 /**
222 186
  * Sets the remote control supported value and initializes or updates the menu
223 187
  * depending on the remote control is supported or not.
@@ -288,27 +252,6 @@ RemoteVideo.prototype._stopRemoteControl = function () {
288 252
     this.updateRemoteVideoMenu(this.isAudioMuted, true);
289 253
 };
290 254
 
291
-RemoteVideo.prototype._muteHandler = function () {
292
-    if (this.isAudioMuted)
293
-        return;
294
-
295
-    RemoteVideo.showMuteParticipantDialog().then(reason => {
296
-        if(reason === MUTED_DIALOG_BUTTON_VALUES.muted) {
297
-            this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
298
-        }
299
-    }).catch(e => {
300
-        //currently shouldn't be called
301
-        logger.error(e);
302
-    });
303
-
304
-    this.popover.forceHide();
305
-};
306
-
307
-RemoteVideo.prototype._kickHandler = function () {
308
-    this.emitter.emit(UIEvents.USER_KICKED, this.id);
309
-    this.popover.forceHide();
310
-};
311
-
312 255
 /**
313 256
  * Get the remote participant's audio element.
314 257
  *
@@ -345,18 +288,10 @@ RemoteVideo.prototype._setAudioVolume = function (newVal) {
345 288
  * @param isMuted the new muted state to update to
346 289
  * @param force to work even if popover is not visible
347 290
  */
348
-RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
291
+RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
349 292
     this.isAudioMuted = isMuted;
350 293
 
351
-    if (!this.popover) {
352
-        return;
353
-    }
354
-
355
-    // generate content, translate it and add it to document only if
356
-    // popover is visible or we force to do so.
357
-    if(this.popover.popoverShown || force) {
358
-        this.popover.updateContent(this._generatePopupContent());
359
-    }
294
+    this._generatePopupContent();
360 295
 };
361 296
 
362 297
 /**
@@ -395,17 +330,9 @@ RemoteVideo.prototype.addRemoteVideoMenu = function () {
395 330
     if (interfaceConfig.filmStripOnly) {
396 331
         return;
397 332
     }
398
-    var spanElement = document.createElement('span');
399
-    spanElement.className = 'remotevideomenu';
400
-
401
-    this.container.appendChild(spanElement);
402 333
 
403
-    var menuElement = document.createElement('i');
404
-    menuElement.className = 'icon-thumb-menu';
405
-    menuElement.title = 'Remote user controls';
406
-    spanElement.appendChild(menuElement);
334
+    this._generatePopupContent();
407 335
 
408
-    this._initPopupMenu(this._generatePopupContent());
409 336
     this.hasRemoteVideoMenu = true;
410 337
 };
411 338
 
@@ -538,6 +465,8 @@ RemoteVideo.prototype.remove = function () {
538 465
 
539 466
     this._unmountIndicators();
540 467
 
468
+    this.removeRemoteVideoMenu();
469
+
541 470
     // Make sure that the large video is updated if are removing its
542 471
     // corresponding small video.
543 472
     this.VideoLayout.updateAfterThumbRemoved(this.id);
@@ -591,17 +520,29 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
591 520
 
592 521
     // Add click handler.
593 522
     let onClickHandler = (event) => {
594
-        let source = event.target || event.srcElement;
595
-
596
-        // ignore click if it was done in popup menu
597
-        if ($(source).parents('.popupmenu').length === 0) {
523
+        const $source = $(event.target || event.srcElement);
524
+        const { classList } = event.target;
525
+
526
+        const clickedOnPopover
527
+            = $source.parents('.connection-info').length > 0;
528
+        const clickedOnPopoverTrigger
529
+            = $source.parents('.popover-trigger').length > 0
530
+                || classList.contains('popover-trigger');
531
+        const clickedOnRemoteMenu
532
+            = $source.parents('.remotevideomenu').length > 0;
533
+
534
+        const ignoreClick = clickedOnPopoverTrigger
535
+            || clickedOnPopover
536
+            || clickedOnRemoteMenu;
537
+
538
+        if (!ignoreClick) {
598 539
             this.VideoLayout.handleVideoThumbClicked(this.id);
599 540
         }
600 541
 
601 542
         // On IE we need to populate this handler on video <object>
602 543
         // and it does not give event instance as an argument,
603 544
         // so we check here for methods.
604
-        if (event.stopPropagation && event.preventDefault) {
545
+        if (event.stopPropagation && event.preventDefault && !ignoreClick) {
605 546
             event.stopPropagation();
606 547
             event.preventDefault();
607 548
         }
@@ -665,8 +606,9 @@ RemoteVideo.prototype.setDisplayName = function(displayName) {
665 606
  */
666 607
 RemoteVideo.prototype.removeRemoteVideoMenu = function() {
667 608
     var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
609
+
668 610
     if (menuSpan.length) {
669
-        this.popover.forceHide();
611
+        ReactDOM.unmountComponentAtNode(menuSpan.get(0));
670 612
         menuSpan.remove();
671 613
         this.hasRemoteVideoMenu = false;
672 614
     }
@@ -740,30 +682,12 @@ RemoteVideo.createContainer = function (spanId) {
740 682
     presenceLabelContainer.className = 'presence-label-container';
741 683
     container.appendChild(presenceLabelContainer);
742 684
 
685
+    const remoteVideoMenuContainer = document.createElement('span');
686
+    remoteVideoMenuContainer.className = 'remotevideomenu';
687
+    container.appendChild(remoteVideoMenuContainer);
688
+
743 689
     var remotes = document.getElementById('filmstripRemoteVideosContainer');
744 690
     return remotes.appendChild(container);
745 691
 };
746 692
 
747
-/**
748
- * Shows 2 button dialog for confirmation from the user for muting remote
749
- * participant.
750
- */
751
-RemoteVideo.showMuteParticipantDialog = function () {
752
-    return new Promise(resolve => {
753
-        APP.UI.messageHandler.openTwoButtonDialog({
754
-            titleKey : "dialog.muteParticipantTitle",
755
-            msgString: "<div data-i18n='dialog.muteParticipantBody'></div>",
756
-            leftButtonKey: "dialog.muteParticipantButton",
757
-            dontShowAgain: {
758
-                id: "dontShowMuteParticipantDialog",
759
-                textKey: "dialog.doNotShowMessageAgain",
760
-                checked: true,
761
-                buttonValues: [true]
762
-            },
763
-            submitFunction: () => resolve(MUTED_DIALOG_BUTTON_VALUES.muted),
764
-            closeFunction: () => resolve(MUTED_DIALOG_BUTTON_VALUES.cancel)
765
-        });
766
-    });
767
-};
768
-
769 693
 export default RemoteVideo;

+ 18
- 15
modules/UI/videolayout/SmallVideo.js 查看文件

@@ -747,21 +747,24 @@ SmallVideo.prototype.updateIndicators = function () {
747 747
 
748 748
     /* jshint ignore:start */
749 749
     ReactDOM.render(
750
-        <div>
751
-            { this._showConnectionIndicator
752
-                ? <ConnectionIndicator
753
-                    connectionStatus = { this._connectionStatus }
754
-                    iconSize = { iconSize }
755
-                    isLocalVideo = { this.isLocal }
756
-                    onHover = { this._onPopoverHover }
757
-                    showMoreLink = { this.isLocal }
758
-                    userID = { this.id } />
759
-                : null }
760
-            { this._showRaisedHand
761
-                ? <RaisedHandIndicator iconSize = { iconSize } /> : null }
762
-            { this._showDominantSpeaker
763
-                ? <DominantSpeakerIndicator iconSize = { iconSize } /> : null }
764
-        </div>,
750
+        <I18nextProvider i18n = { i18next }>
751
+            <div>
752
+                { this._showConnectionIndicator
753
+                    ? <ConnectionIndicator
754
+                        connectionStatus = { this._connectionStatus }
755
+                        isLocalVideo = { this.isLocal }
756
+                        enableStatsDisplay = { !interfaceConfig.filmStripOnly }
757
+                        statsPopoverPosition = { this.statsPopoverLocation }
758
+                        userID = { this.id } />
759
+                    : null }
760
+                { this._showRaisedHand
761
+                    ? <RaisedHandIndicator iconSize = { iconSize } />
762
+                    : null }
763
+                { this._showDominantSpeaker
764
+                    ? <DominantSpeakerIndicator iconSize = { iconSize } />
765
+                    : null }
766
+            </div>
767
+        </I18nextProvider>,
765 768
         indicatorToolbar
766 769
     );
767 770
     /* jshint ignore:end */

+ 20
- 0
react/features/base/participants/actionTypes.js 查看文件

@@ -10,6 +10,26 @@
10 10
  */
11 11
 export const DOMINANT_SPEAKER_CHANGED = Symbol('DOMINANT_SPEAKER_CHANGED');
12 12
 
13
+/**
14
+ * Create an action for removing a participant from the conference.
15
+ *
16
+ * {
17
+ *     type: KICK_PARTICIPANT,
18
+ *     id: string
19
+ * }
20
+ */
21
+export const KICK_PARTICIPANT = Symbol('KICK_PARTICIPANT');
22
+
23
+/**
24
+ * Create an action for muting a remote participant.
25
+ *
26
+ * {
27
+ *     type: MUTE_REMOTE_PARTICIPANT,
28
+ *     id: string
29
+ * }
30
+ */
31
+export const MUTE_REMOTE_PARTICIPANT = Symbol('MUTE_REMOTE_PARTICIPANT');
32
+
13 33
 /**
14 34
  * Create an action for when the local participant's display name is updated.
15 35
  *

+ 34
- 0
react/features/base/participants/actions.js 查看文件

@@ -1,5 +1,7 @@
1 1
 import {
2 2
     DOMINANT_SPEAKER_CHANGED,
3
+    KICK_PARTICIPANT,
4
+    MUTE_REMOTE_PARTICIPANT,
3 5
     PARTICIPANT_DISPLAY_NAME_CHANGED,
4 6
     PARTICIPANT_ID_CHANGED,
5 7
     PARTICIPANT_JOINED,
@@ -50,6 +52,22 @@ export function localParticipantConnectionStatusChanged(connectionStatus) {
50 52
     };
51 53
 }
52 54
 
55
+/**
56
+ * Create an action for removing a participant from the conference.
57
+ *
58
+ * @param {string} id - Participant's ID.
59
+ * @returns {{
60
+ *     type: KICK_PARTICIPANT,
61
+ *     id: string
62
+ * }}
63
+ */
64
+export function kickParticipant(id) {
65
+    return {
66
+        type: KICK_PARTICIPANT,
67
+        id
68
+    };
69
+}
70
+
53 71
 /**
54 72
  * Action to signal that the ID of local participant has changed. It happens
55 73
  * when the local participant joins a new conference or leaves an existing
@@ -106,6 +124,22 @@ export function localParticipantRoleChanged(role) {
106 124
     };
107 125
 }
108 126
 
127
+/**
128
+ * Create an action for muting another participant in the conference.
129
+ *
130
+ * @param {string} id - Participant's ID.
131
+ * @returns {{
132
+ *     type: MUTE_REMOTE_PARTICIPANT,
133
+ *     id: string
134
+ * }}
135
+ */
136
+export function muteRemoteParticipant(id) {
137
+    return {
138
+        type: MUTE_REMOTE_PARTICIPANT,
139
+        id
140
+    };
141
+}
142
+
109 143
 /**
110 144
  * Action to update a participant's connection status.
111 145
  *

+ 31
- 1
react/features/base/participants/middleware.js 查看文件

@@ -7,7 +7,11 @@ import {
7 7
 import { MiddlewareRegistry } from '../redux';
8 8
 
9 9
 import { localParticipantIdChanged } from './actions';
10
-import { PARTICIPANT_DISPLAY_NAME_CHANGED } from './actionTypes';
10
+import {
11
+    KICK_PARTICIPANT,
12
+    MUTE_REMOTE_PARTICIPANT,
13
+    PARTICIPANT_DISPLAY_NAME_CHANGED
14
+} from './actionTypes';
11 15
 import { LOCAL_PARTICIPANT_DEFAULT_ID } from './constants';
12 16
 import { getLocalParticipant } from './functions';
13 17
 
@@ -30,6 +34,32 @@ MiddlewareRegistry.register(store => next => action => {
30 34
         store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
31 35
         break;
32 36
 
37
+    case KICK_PARTICIPANT:
38
+        if (typeof APP !== 'undefined') {
39
+            APP.UI.emitEvent(UIEvents.USER_KICKED, action.id);
40
+        }
41
+        break;
42
+
43
+    case MUTE_REMOTE_PARTICIPANT:
44
+        if (typeof APP !== 'undefined') {
45
+            APP.UI.messageHandler.openTwoButtonDialog({
46
+                titleKey: 'dialog.muteParticipantTitle',
47
+                msgString:
48
+                    '<div data-i18n="dialog.muteParticipantBody"></div>',
49
+                leftButtonKey: 'dialog.muteParticipantButton',
50
+                dontShowAgain: {
51
+                    id: 'dontShowMuteParticipantDialog',
52
+                    textKey: 'dialog.doNotShowMessageAgain',
53
+                    checked: true,
54
+                    buttonValues: [ true ]
55
+                },
56
+                submitFunction: () => {
57
+                    APP.UI.emitEvent(UIEvents.REMOTE_AUDIO_MUTED, action.id);
58
+                }
59
+            });
60
+        }
61
+        break;
62
+
33 63
     // TODO Remove this middleware when the local display name update flow is
34 64
     // fully brought into redux.
35 65
     case PARTICIPANT_DISPLAY_NAME_CHANGED: {

+ 55
- 57
react/features/connection-indicator/components/ConnectionIndicator.js 查看文件

@@ -1,7 +1,6 @@
1
+import AKInlineDialog from '@atlaskit/inline-dialog';
1 2
 import React, { Component } from 'react';
2 3
 
3
-import JitsiPopover from '../../../../modules/UI/util/JitsiPopover';
4
-
5 4
 import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
6 5
 import { ConnectionStatsTable } from '../../connection-stats';
7 6
 
@@ -68,20 +67,21 @@ class ConnectionIndicator extends Component {
68 67
         connectionStatus: React.PropTypes.string,
69 68
 
70 69
         /**
71
-         * Whether or not the displays stats are for local video.
70
+         * Whether or not clicking the indicator should display a popover for
71
+         * more details.
72 72
          */
73
-        isLocalVideo: React.PropTypes.bool,
73
+        enableStatsDisplay: React.PropTypes.bool,
74 74
 
75 75
         /**
76
-         * The callback to invoke when the hover state over the popover changes.
76
+         * Whether or not the displays stats are for local video.
77 77
          */
78
-        onHover: React.PropTypes.func,
78
+        isLocalVideo: React.PropTypes.bool,
79 79
 
80 80
         /**
81
-         * Whether or not the popover should display a link that can toggle
82
-         * a more detailed view of the stats.
81
+         * Relative to the icon from where the popover for more connection
82
+         * details should display.
83 83
          */
84
-        showMoreLink: React.PropTypes.bool,
84
+        statsPopoverPosition: React.PropTypes.string,
85 85
 
86 86
         /**
87 87
          * Invoked to obtain translated strings.
@@ -104,16 +104,6 @@ class ConnectionIndicator extends Component {
104 104
     constructor(props) {
105 105
         super(props);
106 106
 
107
-        /**
108
-         * The internal reference to topmost DOM/HTML element backing the React
109
-         * {@code Component}. Accessed directly for associating an element as
110
-         * the trigger for a popover.
111
-         *
112
-         * @private
113
-         * @type {HTMLDivElement}
114
-         */
115
-        this._rootElement = null;
116
-
117 107
         this.state = {
118 108
             /**
119 109
              * Whether or not the popover content should display additional
@@ -134,12 +124,14 @@ class ConnectionIndicator extends Component {
134 124
 
135 125
         // Bind event handlers so they are only bound once for every instance.
136 126
         this._onStatsUpdated = this._onStatsUpdated.bind(this);
127
+        this._onStatsClose = this._onStatsClose.bind(this);
128
+        this._onStatsToggle = this._onStatsToggle.bind(this);
129
+        this._onStatsUpdated = this._onStatsUpdated.bind(this);
137 130
         this._onToggleShowMore = this._onToggleShowMore.bind(this);
138
-        this._setRootElement = this._setRootElement.bind(this);
139 131
     }
140 132
 
141 133
     /**
142
-     * Creates a popover instance to display when the component is hovered.
134
+     * Starts listening for stat updates.
143 135
      *
144 136
      * @inheritdoc
145 137
      * returns {void}
@@ -147,20 +139,10 @@ class ConnectionIndicator extends Component {
147 139
     componentDidMount() {
148 140
         statsEmitter.subscribeToClientStats(
149 141
             this.props.userID, this._onStatsUpdated);
150
-
151
-        this.popover = new JitsiPopover($(this._rootElement), {
152
-            content: this._renderStatisticsTable(),
153
-            skin: 'black',
154
-            position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
155
-        });
156
-
157
-        this.popover.addOnHoverPopover(this.props.onHover);
158 142
     }
159 143
 
160 144
     /**
161
-     * Updates the contents of the popover. This is done manually because the
162
-     * popover is not a React Component yet and so is not automatiucally aware
163
-     * of changed data.
145
+     * Updates which user's stats are being listened to.
164 146
      *
165 147
      * @inheritdoc
166 148
      * returns {void}
@@ -172,22 +154,17 @@ class ConnectionIndicator extends Component {
172 154
             statsEmitter.subscribeToClientStats(
173 155
                 this.props.userID, this._onStatsUpdated);
174 156
         }
175
-
176
-        this.popover.updateContent(this._renderStatisticsTable());
177 157
     }
178 158
 
179 159
     /**
180
-     * Cleans up any popover instance that is linked to the component.
160
+     * Sets the state to hide the Statistics Table popover.
181 161
      *
182
-     * @inheritdoc
183
-     * returns {void}
162
+     * @private
163
+     * @returns {void}
184 164
      */
185 165
     componentWillUnmount() {
186 166
         statsEmitter.unsubscribeToClientStats(
187 167
             this.props.userID, this._onStatsUpdated);
188
-
189
-        this.popover.forceHide();
190
-        this.popover.remove();
191 168
     }
192 169
 
193 170
     /**
@@ -198,16 +175,48 @@ class ConnectionIndicator extends Component {
198 175
      */
199 176
     render() {
200 177
         return (
201
-            <div
202
-                className = 'connection-indicator indicator'
203
-                ref = { this._setRootElement }>
204
-                <div className = 'connection indicatoricon'>
205
-                    { this._renderIcon() }
206
-                </div>
178
+            <div className = 'connection-indicator-container'>
179
+                <AKInlineDialog
180
+                    content = { this._renderStatisticsTable() }
181
+                    isOpen = { this.state.showStats }
182
+                    onClose = { this._onStatsClose }
183
+                    position = { this.props.statsPopoverPosition }>
184
+                    <div
185
+                        className = 'popover-trigger'
186
+                        onClick = { this._onStatsToggle }>
187
+                        <div className = 'connection-indicator indicator'>
188
+                            <div className = 'connection indicatoricon'>
189
+                                { this._renderIcon() }
190
+                            </div>
191
+                        </div>
192
+                    </div>
193
+                </AKInlineDialog>
207 194
             </div>
208 195
         );
209 196
     }
210 197
 
198
+    /**
199
+     * Sets the state not to show the Statistics Table popover.
200
+     *
201
+     * @private
202
+     * @returns {void}
203
+     */
204
+    _onStatsClose() {
205
+        this.setState({ showStats: false });
206
+    }
207
+
208
+    /**
209
+     * Sets the state to show or hide the Statistics Table popover.
210
+     *
211
+     * @private
212
+     * @returns {void}
213
+     */
214
+    _onStatsToggle() {
215
+        if (this.props.enableStatsDisplay) {
216
+            this.setState({ showStats: !this.state.showStats });
217
+        }
218
+    }
219
+
211 220
     /**
212 221
      * Callback invoked when new connection stats associated with the passed in
213 222
      * user ID are available. Will update the component's display of current
@@ -314,17 +323,6 @@ class ConnectionIndicator extends Component {
314 323
                 transport = { transport } />
315 324
         );
316 325
     }
317
-
318
-    /**
319
-     * Sets an internal reference to the component's root element.
320
-     *
321
-     * @param {Object} element - The highest DOM element in the component.
322
-     * @private
323
-     * @returns {void}
324
-     */
325
-    _setRootElement(element) {
326
-        this._rootElement = element;
327
-    }
328 326
 }
329 327
 
330 328
 export default ConnectionIndicator;

+ 1
- 1
react/features/connection-stats/components/ConnectionStatsTable.js 查看文件

@@ -292,7 +292,7 @@ class ConnectionStatsTable extends Component {
292 292
 
293 293
         return (
294 294
             <a
295
-                className = 'jitsipopover__showmore link'
295
+                className = 'showmore link'
296 296
                 onClick = { this.props.onShowMore } >
297 297
                 { this.props.t(translationKey) }
298 298
             </a>

+ 1
- 1
react/features/filmstrip/components/web/BaseIndicator.js 查看文件

@@ -27,7 +27,7 @@ class BaseIndicator extends Component {
27 27
         iconClassName: React.PropTypes.string,
28 28
 
29 29
         /**
30
-         * The front size for the icon.
30
+         * The font size for the icon.
31 31
          */
32 32
         iconSize: React.PropTypes.string,
33 33
 

+ 41
- 4
react/features/remote-video-menu/components/KickButton.js 查看文件

@@ -1,6 +1,8 @@
1 1
 import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
2 3
 
3 4
 import { translate } from '../../base/i18n';
5
+import { kickParticipant } from '../../base/participants';
4 6
 
5 7
 import RemoteVideoMenuButton from './RemoteVideoMenuButton';
6 8
 
@@ -18,7 +20,13 @@ class KickButton extends Component {
18 20
      */
19 21
     static propTypes = {
20 22
         /**
21
-         * The callback to invoke when the component is clicked.
23
+         * Invoked to signal the participant with the passed in participantID
24
+         * should be removed from the conference.
25
+         */
26
+        dispatch: React.PropTypes.func,
27
+
28
+        /**
29
+         * Callback to invoke when {@code KickButton} is clicked.
22 30
          */
23 31
         onClick: React.PropTypes.func,
24 32
 
@@ -33,6 +41,19 @@ class KickButton extends Component {
33 41
         t: React.PropTypes.func
34 42
     };
35 43
 
44
+    /**
45
+     * Initializes a new {@code KickButton} instance.
46
+     *
47
+     * @param {Object} props - The read-only React Component props with which
48
+     * the new instance is to be initialized.
49
+     */
50
+    constructor(props) {
51
+        super(props);
52
+
53
+        // Bind event handlers so they are only bound once for every instance.
54
+        this._onClick = this._onClick.bind(this);
55
+    }
56
+
36 57
     /**
37 58
      * Implements React's {@link Component#render()}.
38 59
      *
@@ -40,16 +61,32 @@ class KickButton extends Component {
40 61
      * @returns {ReactElement}
41 62
      */
42 63
     render() {
43
-        const { onClick, participantID, t } = this.props;
64
+        const { participantID, t } = this.props;
44 65
 
45 66
         return (
46 67
             <RemoteVideoMenuButton
47 68
                 buttonText = { t('videothumbnail.kick') }
48 69
                 iconClass = 'icon-kick'
49 70
                 id = { `ejectlink_${participantID}` }
50
-                onClick = { onClick } />
71
+                onClick = { this._onClick } />
51 72
         );
52 73
     }
74
+
75
+    /**
76
+     * Remove the participant with associated participantID from the conference.
77
+     *
78
+     * @private
79
+     * @returns {void}
80
+     */
81
+    _onClick() {
82
+        const { dispatch, onClick, participantID } = this.props;
83
+
84
+        dispatch(kickParticipant(participantID));
85
+
86
+        if (onClick) {
87
+            onClick();
88
+        }
89
+    }
53 90
 }
54 91
 
55
-export default translate(KickButton);
92
+export default translate(connect()(KickButton));

+ 42
- 4
react/features/remote-video-menu/components/MuteButton.js 查看文件

@@ -1,6 +1,8 @@
1 1
 import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
2 3
 
3 4
 import { translate } from '../../base/i18n';
5
+import { muteRemoteParticipant } from '../../base/participants';
4 6
 
5 7
 import RemoteVideoMenuButton from './RemoteVideoMenuButton';
6 8
 
@@ -17,13 +19,19 @@ class MuteButton extends Component {
17 19
      * @static
18 20
      */
19 21
     static propTypes = {
22
+        /**
23
+         * Invoked to send a request for muting the participant with the passed
24
+         * in participantID.
25
+         */
26
+        dispatch: React.PropTypes.func,
27
+
20 28
         /**
21 29
          * Whether or not the participant is currently audio muted.
22 30
          */
23 31
         isAudioMuted: React.PropTypes.bool,
24 32
 
25 33
         /**
26
-         * The callback to invoke when the component is clicked.
34
+         * Callback to invoke when {@code MuteButton} is clicked.
27 35
          */
28 36
         onClick: React.PropTypes.func,
29 37
 
@@ -38,6 +46,19 @@ class MuteButton extends Component {
38 46
         t: React.PropTypes.func
39 47
     };
40 48
 
49
+    /**
50
+     * Initializes a new {@code MuteButton} instance.
51
+     *
52
+     * @param {Object} props - The read-only React Component props with which
53
+     * the new instance is to be initialized.
54
+     */
55
+    constructor(props) {
56
+        super(props);
57
+
58
+        // Bind event handlers so they are only bound once for every instance.
59
+        this._onClick = this._onClick.bind(this);
60
+    }
61
+
41 62
     /**
42 63
      * Implements React's {@link Component#render()}.
43 64
      *
@@ -45,7 +66,7 @@ class MuteButton extends Component {
45 66
      * @returns {ReactElement}
46 67
      */
47 68
     render() {
48
-        const { isAudioMuted, onClick, participantID, t } = this.props;
69
+        const { isAudioMuted, participantID, t } = this.props;
49 70
         const muteConfig = isAudioMuted ? {
50 71
             translationKey: 'videothumbnail.muted',
51 72
             muteClassName: 'mutelink disabled'
@@ -60,9 +81,26 @@ class MuteButton extends Component {
60 81
                 displayClass = { muteConfig.muteClassName }
61 82
                 iconClass = 'icon-mic-disabled'
62 83
                 id = { `mutelink_${participantID}` }
63
-                onClick = { onClick } />
84
+                onClick = { this._onClick } />
64 85
         );
65 86
     }
87
+
88
+    /**
89
+     * Dispatches a request to mute the participant with the passed in
90
+     * participantID.
91
+     *
92
+     * @private
93
+     * @returns {void}
94
+     */
95
+    _onClick() {
96
+        const { dispatch, onClick, participantID } = this.props;
97
+
98
+        dispatch(muteRemoteParticipant(participantID));
99
+
100
+        if (onClick) {
101
+            onClick();
102
+        }
103
+    }
66 104
 }
67 105
 
68
-export default translate(MuteButton);
106
+export default translate(connect()(MuteButton));

+ 194
- 0
react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js 查看文件

@@ -0,0 +1,194 @@
1
+import AKInlineDialog from '@atlaskit/inline-dialog';
2
+import React, { Component } from 'react';
3
+
4
+import {
5
+    MuteButton,
6
+    KickButton,
7
+    RemoteControlButton,
8
+    RemoteVideoMenu,
9
+    VolumeSlider
10
+} from './';
11
+
12
+declare var $: Object;
13
+declare var interfaceConfig: Object;
14
+
15
+/**
16
+ * React {@code Component} for displaying an icon associated with opening the
17
+ * the {@code RemoteVideoMenu}.
18
+ *
19
+ * @extends {Component}
20
+ */
21
+class RemoteVideoMenuTriggerButton extends Component {
22
+    static propTypes = {
23
+        /**
24
+         * A value between 0 and 1 indicating the volume of the participant's
25
+         * audio element.
26
+         */
27
+        initialVolumeValue: React.PropTypes.number,
28
+
29
+        /**
30
+         * Whether or not the participant is currently muted.
31
+         */
32
+        isAudioMuted: React.PropTypes.bool,
33
+
34
+        /**
35
+         * Whether or not the participant is a conference moderator.
36
+         */
37
+        isModerator: React.PropTypes.bool,
38
+
39
+        /**
40
+         * Callback to invoke when the popover has been displayed.
41
+         */
42
+        onMenuDisplay: React.PropTypes.func,
43
+
44
+        /**
45
+         * Callback to invoke choosing to start a remote control session with
46
+         * the participant.
47
+         */
48
+        onRemoteControlToggle: React.PropTypes.func,
49
+
50
+        /**
51
+         * Callback to invoke when changing the level of the participant's
52
+         * audio element.
53
+         */
54
+        onVolumeChange: React.PropTypes.func,
55
+
56
+        /**
57
+         * The ID for the participant on which the remote video menu will act.
58
+         */
59
+        participantID: React.PropTypes.string,
60
+
61
+        /**
62
+         * The current state of the participant's remote control session.
63
+         */
64
+        remoteControlState: React.PropTypes.number
65
+    };
66
+
67
+    /**
68
+     * Initializes a new {#@code RemoteVideoMenuTriggerButton} instance.
69
+     *
70
+     * @param {Object} props - The read-only properties with which the new
71
+     * instance is to be initialized.
72
+     */
73
+    constructor(props) {
74
+        super(props);
75
+
76
+        this.state = {
77
+            showRemoteMenu: false
78
+        };
79
+
80
+        /**
81
+         * The internal reference to topmost DOM/HTML element backing the React
82
+         * {@code Component}. Accessed directly for associating an element as
83
+         * the trigger for a popover.
84
+         *
85
+         * @private
86
+         * @type {HTMLDivElement}
87
+         */
88
+        this._rootElement = null;
89
+
90
+        // Bind event handlers so they are only bound once for every instance.
91
+        this._onRemoteMenuClose = this._onRemoteMenuClose.bind(this);
92
+        this._onRemoteMenuToggle = this._onRemoteMenuToggle.bind(this);
93
+    }
94
+
95
+    /**
96
+     * Implements React's {@link Component#render()}.
97
+     *
98
+     * @inheritdoc
99
+     * @returns {ReactElement}
100
+     */
101
+    render() {
102
+        return (
103
+            <AKInlineDialog
104
+                content = { this._renderRemoteVideoMenu() }
105
+                isOpen = { this.state.showRemoteMenu }
106
+                onClose = { this._onRemoteMenuClose }
107
+                position = { interfaceConfig.VERTICAL_FILMSTRIP
108
+                    ? 'left middle' : 'top center' }
109
+                shouldFlip = { true }>
110
+                <span
111
+                    className = 'popover-trigger remote-video-menu-trigger'
112
+                    onClick = { this._onRemoteMenuToggle }>
113
+                    <i
114
+                        className = 'icon-thumb-menu'
115
+                        title = 'Remote user controls' />
116
+                </span>
117
+            </AKInlineDialog>
118
+        );
119
+    }
120
+
121
+    /**
122
+     * Closes the {@code RemoteVideoMenu}.
123
+     *
124
+     * @private
125
+     * @returns {void}
126
+     */
127
+    _onRemoteMenuClose() {
128
+        this.setState({ showRemoteMenu: false });
129
+    }
130
+
131
+    /**
132
+     * Opens or closes the {@code RemoteVideoMenu}.
133
+     *
134
+     * @private
135
+     * @returns {void}
136
+     */
137
+    _onRemoteMenuToggle() {
138
+        const willShowRemoteMenu = !this.state.showRemoteMenu;
139
+
140
+        if (willShowRemoteMenu) {
141
+            this.props.onMenuDisplay();
142
+        }
143
+
144
+        this.setState({ showRemoteMenu: willShowRemoteMenu });
145
+    }
146
+
147
+    /**
148
+     * Creates a new {@code RemoteVideoMenu} with buttons for interacting with
149
+     * the remote participant.
150
+     *
151
+     * @private
152
+     * @returns {ReactElement}
153
+     */
154
+    _renderRemoteVideoMenu() {
155
+        const {
156
+            initialVolumeValue,
157
+            isAudioMuted,
158
+            isModerator,
159
+            onRemoteControlToggle,
160
+            onVolumeChange,
161
+            remoteControlState,
162
+            participantID
163
+        } = this.props;
164
+
165
+        return (
166
+            <RemoteVideoMenu id = { participantID }>
167
+                { isModerator
168
+                    ? <MuteButton
169
+                        isAudioMuted = { isAudioMuted }
170
+                        onClick = { this._onRemoteMenuClose }
171
+                        participantID = { participantID } />
172
+                    : null }
173
+                { isModerator
174
+                    ? <KickButton
175
+                        onClick = { this._onRemoteMenuClose }
176
+                        participantID = { participantID } />
177
+                    : null }
178
+                { remoteControlState
179
+                    ? <RemoteControlButton
180
+                        onClick = { onRemoteControlToggle }
181
+                        participantID = { participantID }
182
+                        remoteControlState = { remoteControlState } />
183
+                    : null }
184
+                { onVolumeChange
185
+                    ? <VolumeSlider
186
+                        initialValue = { initialVolumeValue }
187
+                        onChange = { onVolumeChange } />
188
+                    : null }
189
+            </RemoteVideoMenu>
190
+        );
191
+    }
192
+}
193
+
194
+export default RemoteVideoMenuTriggerButton;

+ 3
- 0
react/features/remote-video-menu/components/index.js 查看文件

@@ -5,4 +5,7 @@ export {
5 5
     default as RemoteControlButton
6 6
 } from './RemoteControlButton';
7 7
 export { default as RemoteVideoMenu } from './RemoteVideoMenu';
8
+export {
9
+    default as RemoteVideoMenuTriggerButton
10
+} from './RemoteVideoMenuTriggerButton';
8 11
 export { default as VolumeSlider } from './VolumeSlider';

Loading…
取消
儲存