Bladeren bron

feat(thumbnail) Video thumbnails redesign and refactor (#10351)

Update video thumbnail design
Update design of indicators
In filmstrip view move Screen Sharing indicator to the top
Removed dominant speaker indicator
Use ContextMenu component for the connection stats popover
Combine Remove video menu and Meeting participant context menu into one component
Moved some styles from SCSS to JSS
Fix mobile avatars too big
Fix mobile horizontal scroll
Created button for Send to breakout room action
master
Robert Pintilii 3 jaren geleden
bovenliggende
commit
91437c50e3
No account linked to committer's email address
66 gewijzigde bestanden met toevoegingen van 1864 en 2560 verwijderingen
  1. 0
    8
      css/_atlaskit_overrides.scss
  2. 0
    9
      css/_drawer.scss
  3. 0
    5
      css/_popover.scss
  4. 10
    114
      css/_popup_menu.scss
  5. 0
    12
      css/_variables.scss
  6. 1
    380
      css/_videolayout_default.scss
  7. 1
    28
      css/filmstrip/_small_video.scss
  8. 0
    11
      css/filmstrip/_tile_view.scss
  9. 0
    12
      css/filmstrip/_tile_view_overrides.scss
  10. 0
    66
      css/filmstrip/_vertical_filmstrip_overrides.scss
  11. 0
    4
      css/themes/_light.scss
  12. 1
    1
      interface_config.js
  13. 2
    2
      lang/main.json
  14. 18
    4
      react/features/base/components/context-menu/ContextMenu.js
  15. 129
    0
      react/features/base/components/context-menu/ContextMenuItem.js
  16. 5
    89
      react/features/base/components/context-menu/ContextMenuItemGroup.js
  17. 2
    2
      react/features/base/icons/svg/crown.svg
  18. 3
    10
      react/features/base/icons/svg/mute-everyone-else.svg
  19. 2
    2
      react/features/base/icons/svg/share-desktop.svg
  20. 3
    13
      react/features/base/popover/components/Popover.web.js
  21. 51
    58
      react/features/base/react/components/web/BaseIndicator.js
  22. 74
    51
      react/features/connection-indicator/components/web/ConnectionIndicator.js
  23. 37
    11
      react/features/connection-stats/components/ConnectionStatsTable.js
  24. 66
    9
      react/features/display-name/components/web/DisplayName.js
  25. 12
    22
      react/features/filmstrip/components/web/AudioMutedIndicator.js
  26. 0
    51
      react/features/filmstrip/components/web/DominantSpeakerIndicator.js
  27. 10
    23
      react/features/filmstrip/components/web/ModeratorIndicator.js
  28. 45
    23
      react/features/filmstrip/components/web/RaisedHandIndicator.js
  29. 1
    2
      react/features/filmstrip/components/web/ScreenShareIndicator.js
  30. 13
    35
      react/features/filmstrip/components/web/StatusIndicators.js
  31. 186
    443
      react/features/filmstrip/components/web/Thumbnail.js
  32. 47
    0
      react/features/filmstrip/components/web/ThumbnailAudioIndicator.js
  33. 84
    0
      react/features/filmstrip/components/web/ThumbnailBottomIndicators.js
  34. 132
    0
      react/features/filmstrip/components/web/ThumbnailTopIndicators.js
  35. 12
    2
      react/features/filmstrip/components/web/ThumbnailWrapper.js
  36. 69
    0
      react/features/filmstrip/components/web/VideoMenuTriggerButton.js
  37. 0
    43
      react/features/filmstrip/components/web/VideoMutedIndicator.js
  38. 0
    2
      react/features/filmstrip/components/web/index.js
  39. 33
    44
      react/features/filmstrip/constants.js
  40. 57
    13
      react/features/filmstrip/functions.web.js
  41. 15
    447
      react/features/participants-pane/components/web/MeetingParticipantContextMenu.js
  42. 1
    1
      react/features/participants-pane/components/web/RaisedHandIndicator.js
  43. 2
    0
      react/features/participants-pane/constants.js
  44. 1
    1
      react/features/toolbox/components/MuteEveryonesVideoButton.js
  45. 3
    1
      react/features/video-menu/components/AbstractMuteButton.js
  46. 1
    1
      react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js
  47. 49
    0
      react/features/video-menu/components/web/AskToUnmuteButton.js
  48. 7
    8
      react/features/video-menu/components/web/ConnectionStatusButton.js
  49. 13
    6
      react/features/video-menu/components/web/FlipLocalVideoButton.js
  50. 7
    8
      react/features/video-menu/components/web/GrantModeratorButton.js
  51. 14
    7
      react/features/video-menu/components/web/HideSelfViewVideoButton.js
  52. 8
    8
      react/features/video-menu/components/web/KickButton.js
  53. 65
    25
      react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js
  54. 13
    16
      react/features/video-menu/components/web/MuteButton.js
  55. 6
    8
      react/features/video-menu/components/web/MuteEveryoneElseButton.js
  56. 6
    8
      react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js
  57. 13
    17
      react/features/video-menu/components/web/MuteVideoButton.js
  58. 332
    0
      react/features/video-menu/components/web/ParticipantContextMenu.js
  59. 6
    7
      react/features/video-menu/components/web/PrivateMessageMenuButton.js
  60. 9
    10
      react/features/video-menu/components/web/RemoteControlButton.js
  61. 58
    193
      react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js
  62. 50
    0
      react/features/video-menu/components/web/SendToRoomButton.js
  63. 0
    51
      react/features/video-menu/components/web/VideoMenu.js
  64. 0
    111
      react/features/video-menu/components/web/VideoMenuButton.js
  65. 78
    21
      react/features/video-menu/components/web/VolumeSlider.js
  66. 1
    1
      react/features/video-menu/components/web/index.js

+ 0
- 8
css/_atlaskit_overrides.scss Bestand weergeven

@@ -24,14 +24,6 @@
24 24
     bottom: calc(#{$newToolbarSizeWithPadding}) !important;
25 25
 }
26 26
 
27
-/**
28
- * Override @atlaskit/theme styling for the top toolbar so it displays over
29
- * the video thumbnail while obscuring as little as possible.
30
- */
31
-.videocontainer__toptoolbar > div > div {
32
-    background: none;
33
-}
34
-
35 27
 
36 28
 /**
37 29
  * Keep overflow menu within screen vertical bounds and make it scrollable.

+ 0
- 9
css/_drawer.scss Bestand weergeven

@@ -42,15 +42,6 @@
42 42
         }
43 43
     }
44 44
 
45
-    .popupmenu {
46
-        margin: auto;
47
-        width: 100%;
48
-    }
49
-
50
-    .popupmenu__item {
51
-        height: 48px;
52
-    }
53
-
54 45
     &#{&} .overflow-menu {
55 46
         margin: auto;
56 47
         font-size: 1.2em;

+ 0
- 5
css/_popover.scss Bestand weergeven

@@ -43,10 +43,5 @@
43 43
 
44 44
 .popover {
45 45
     margin: -16px -24px;
46
-    padding: 16px 24px;
47 46
     z-index: $popoverZ;
48 47
 }
49
-
50
-.padded-content {
51
-    padding: 4px 8px;
52
-}

+ 10
- 114
css/_popup_menu.scss Bestand weergeven

@@ -2,122 +2,18 @@
2 2
 * Initialize
3 3
 **/
4 4
 
5
-.popupmenu {
6
-    background-color: $menuBG;
7
-    border-radius: 3px;
8
-    list-style-type: none;
9
-    min-width: 150px;
10
-    text-align: left;
11
-    padding: 0px;
12
-    white-space: nowrap;
13
-
14
-    &__item {
15
-        list-style-type: none;
16
-        height: 35px;
17
-    }
18
-
19
-    // Link Appearance
20
-    &__link,
21
-    &__contents {
22
-        display: block;
23
-        box-sizing: border-box;
24
-        text-decoration: none;
25
-        height: 100%;
26
-        font-size: 9pt;
27
-        width: 100%;
28
-        cursor: pointer;
29
-        padding: 0 5px;
30
-        color: $popupMenuColor;
31
-
32
-        &:hover {
33
-            background-color: $popupMenuHoverBackground;
34
-            color: $popupMenuHoverColor;
35
-        }
36
-
37
-        &.disabled {
38
-            pointer-events: none;
39
-        }
40
-    }
41
-
42
-    &__list {
43
-        margin: 0;
44
-        padding: 0;
45
-    }
46
-
47
-    &__text {
48
-        display: inline-block;
49
-        margin-left: 8px;
50
-        vertical-align: middle;
51
-    }
52
-
53
-    &__link {
54
-        i {
55
-            cursor: pointer;
56
-        }
57
-    }
58
-
59
-    &__contents {
60
-        display: flex;
61
-
62
-        /**
63
-         * Positioning styles on the slider and its container are used to make
64
-         * the container fit the popup width, by removing the slider from the
65
-         * page flow, and then making the slider fit the container.
66
-         */
67
-        .popupmenu__slider_container {
68
-            position: relative;
69
-            width: 100%;
70
-
71
-            .popupmenu__slider {
72
-                position: absolute;
73
-                top: 50%;
74
-                transform: translate(0, -50%);
75
-                width: 100%;
76
-
77
-                &::-webkit-slider-runnable-track {
78
-                    background-color: $popupSliderColor;
79
-                }
80
-
81
-                &::-moz-range-track {
82
-                    background-color: $popupSliderColor;
83
-                }
84
-
85
-                &::-ms-fill-lower {
86
-                    background-color: $popupSliderColor;
87
-                }
5
+.popupmenu__contents {
6
+    .popupmenu__volume-slider {
7
+            &::-webkit-slider-runnable-track {
8
+                background-color: $popupSliderColor;
88 9
             }
89
-        }
90
-    }
91 10
 
92
-    &__icon {
93
-        vertical-align: middle;
94
-        position: relative;
95
-        display: inline-block;
96
-        min-width: 20px;
97
-        height: 100%;
98
-        padding-right: 10px;
11
+            &::-moz-range-track {
12
+                background-color: $popupSliderColor;
13
+            }
99 14
 
100
-        > * {
101
-            @include absoluteAligning();
15
+            &::-ms-fill-lower {
16
+                background-color: $popupSliderColor;
17
+            }
102 18
         }
103
-    }
104
-
105
-    .icon-kick,
106
-    .icon-play,
107
-    .icon-stop {
108
-        font-size: 8pt;
109
-    }
110
-}
111
-
112
-/**
113
- * Override reset css styling modifying all lists and set negative margin to
114
- * reduce the visibility of padding on AtlasKit
115
- * InlineDialogs.
116
- */
117
-ul.popupmenu {
118
-    margin: -16px -24px;
119
-}
120
-
121
-span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
122
-    display:block !important;
123 19
 }

+ 0
- 12
css/_variables.scss Bestand weergeven

@@ -13,9 +13,6 @@ $hangupFontSize: 2em;
13 13
  */
14 14
 
15 15
 // Video layout.
16
-$thumbnailToolbarHeight: 22px;
17
-$thumbnailIndicatorBorder: 2px;
18
-$thumbnailIndicatorSize: $thumbnailToolbarHeight;
19 16
 $thumbnailVideoMargin: 2px;
20 17
 $thumbnailsBorder: 2px;
21 18
 $thumbnailVideoBorder: 2px;
@@ -56,19 +53,12 @@ $overflowMenuItemBackground: #36383C;
56 53
 /**
57 54
  * Video layout
58 55
  */
59
-$videoThumbnailHovered: rgba(22, 94, 204, .4);
60
-$videoThumbnailSelected: #165ECC;
61 56
 $participantNameColor: #fff;
62
-$thumbnailPictogramColor: #fff;
63
-$dominantSpeakerBg: #165ecc;
64
-$raiseHandBg: #F8AE1A;
65 57
 $audioLevelBg: #44A5FF;
66
-$connectionIndicatorBg: #165ecc;
67 58
 $audioLevelShadow: rgba(9, 36, 77, 0.9);
68 59
 $videoStateIndicatorColor: $defaultColor;
69 60
 $videoStateIndicatorBackground: $toolbarBackground;
70 61
 $videoStateIndicatorSize: 40px;
71
-$remoteVideoMenuIconMargin: initial;
72 62
 
73 63
 /**
74 64
  * Feedback Modal
@@ -102,7 +92,6 @@ $sidebarWidth: 315px;
102 92
  * Misc.
103 93
  */
104 94
 $borderRadius: 4px;
105
-$popoverMenuPadding: 13px;
106 95
 $happySoftwareBackground: transparent;
107 96
 $desktopAppDragBarHeight: 25px;
108 97
 $scrollHeight: 7px;
@@ -118,7 +107,6 @@ $toolbarBackgroundZ: 4;
118 107
 $labelsZ: 5;
119 108
 $subtitlesZ: 7;
120 109
 $popoverZ: 8;
121
-$zindex10: 10;
122 110
 $reloadZ: 20;
123 111
 $poweredByZ: 100;
124 112
 $ringingZ: 300;

+ 1
- 380
css/_videolayout_default.scss Bestand weergeven

@@ -43,165 +43,7 @@
43 43
 .videocontainer {
44 44
     position: relative;
45 45
     text-align: center;
46
-
47
-    &__background {
48
-        @include topLeft();
49
-        background-color: black;
50
-        border-radius: $borderRadius;
51
-        width: 100%;
52
-        height: 100%;
53
-    }
54
-
55
-    /**
56
-     * The toolbar of the video thumbnail.
57
-     */
58
-    &__toolbar,
59
-    &__toptoolbar {
60
-        position: absolute;
61
-        left: 0;
62
-        pointer-events: none;
63
-        z-index: $zindex10;
64
-        width: 100%;
65
-        box-sizing: border-box; // Includes the padding in the 100% width.
66
-
67
-        /**
68
-         * FIXME (lenny): Disabling pointer-events is a pretty big sin that
69
-         * sidesteps the problems. There are z-index wars occurring within
70
-         * videocontainer and AtlasKit Tooltips rely on their parent z-indexe
71
-         * being higher than whatever they need to appear over. So set a higher
72
-         * z-index for the tooltip containers but make any empty space not block
73
-         * mouse overs for various mouseover triggers.
74
-         */
75
-        pointer-events: none;
76
-
77
-        * {
78
-            pointer-events: auto;
79
-        }
80
-
81
-        .indicator-container {
82
-            display: inline-block;
83
-            float: left;
84
-            pointer-events: all;
85
-        }
86
-    }
87
-
88
-    &__toolbar {
89
-        bottom: 0;
90
-        padding: 0 5px 0 5px;
91
-    }
92
-
93
-    &__toptoolbar {
94
-        $toolbarIconMargin: 5px;
95
-        top: 0;
96
-        padding-bottom: 0;
97
-        /**
98
-         * Override text-align center as icons need to be left justified.
99
-         */
100
-        text-align: left;
101
-
102
-        /**
103
-         * Intentionally use margin on the icon itself as AtlasKit InlineDialog
104
-         * positioning depends on the trigger (indicator icon).
105
-         */
106
-        .indicator {
107
-            margin-left: 5px;
108
-            margin-top: $toolbarIconMargin;
109
-        }
110
-
111
-        .indicator-container:nth-child(1) .indicator {
112
-            margin-left: $toolbarIconMargin;
113
-        }
114
-
115
-        .indicator-container {
116
-            display: inline-block;
117
-            vertical-align: top;
118
-
119
-            .popover-trigger {
120
-                display: inline-block;
121
-            }
122
-        }
123
-
124
-        .connection-indicator,
125
-        .indicator {
126
-            position: relative;
127
-            font-size: 8px;
128
-            text-align: center;
129
-            line-height: $thumbnailIndicatorSize;
130
-            padding: 0;
131
-            @include circle($thumbnailIndicatorSize);
132
-            box-sizing: border-box;
133
-            z-index: $zindex3;
134
-            background: $dominantSpeakerBg;
135
-            color: $thumbnailPictogramColor;
136
-            border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
137
-
138
-            .indicatoricon {
139
-                @include absoluteAligning();
140
-            }
141
-
142
-            .connection {
143
-                position: relative;
144
-                display: inline-block;
145
-                margin: 0 auto;
146
-                left: 0;
147
-                @include transform(translate(0, -50%));
148
-
149
-                &_empty,
150
-                &_lost
151
-                {
152
-                    color: #8B8B8B;/*#FFFFFF*/
153
-                    overflow: hidden;
154
-                }
155
-
156
-                &_full
157
-                {
158
-                    @include topLeft();
159
-                    color: #FFFFFF;/*#15A1ED*/
160
-                    overflow: hidden;
161
-                }
162
-
163
-                &_ninja
164
-                {
165
-                    font-size: 1.5em;
166
-                }
167
-            }
168
-
169
-            .icon-gsm-bars {
170
-                cursor: pointer;
171
-                font-size: 1em;
172
-            }
173
-        }
174
-
175
-        .hide-connection-indicator {
176
-            display: none;
177
-        }
178
-    }
179
-
180
-    &__hoverOverlay {
181
-        background: rgba(0,0,0,.6);
182
-        border-radius: $borderRadius;
183
-        position: absolute;
184
-        top: 0px;
185
-        left: 0px;
186
-        width: 100%;
187
-        height: 100%;
188
-        visibility: hidden;
189
-        z-index: $zindex2;
190
-    }
191
-
192
-    &__participant-name {
193
-        color: #fff;
194
-        background-color: rgba(0,0,0,.4);
195
-        padding: 3px 7px;
196
-        border-radius: 3px;
197
-        max-width: calc(100% - 32px);
198
-        text-overflow: ellipsis;
199
-        overflow: hidden;
200
-        white-space: nowrap;
201
-        height: 16px;
202
-        display: inline-block;
203
-        text-align: right;
204
-    }
46
+    overflow: 'hidden';
205 47
 
206 48
     @media (min-width: 581px) {
207 49
         &.shift-right {
@@ -288,16 +130,6 @@
288 130
     z-index: $zindex0;
289 131
 }
290 132
 
291
-/**
292
- * Positions video thumbnail display name and editor.
293
- */
294
-#alwaysOnTop .displayname,
295
-.videocontainer .displayname,
296
-.videocontainer .editdisplayname {
297
-    font-weight: 100;
298
-    color: $participantNameColor;
299
-}
300
-
301 133
 #alwaysOnTop .displayname {
302 134
     font-size: 15px;
303 135
     position: inherit;
@@ -307,146 +139,6 @@
307 139
     margin-top: 10px;
308 140
 }
309 141
 
310
-/**
311
- * Positions video thumbnail display name editor.
312
- */
313
-.videocontainer .editdisplayname {
314
-    outline: none;
315
-    border: none;
316
-    background: none;
317
-    box-shadow: none;
318
-    padding: 0;
319
-}
320
-
321
-#localVideoContainer .displayname:hover {
322
-    cursor: text;
323
-}
324
-
325
-.videocontainer .displayname {
326
-    pointer-events: none;
327
-    padding: 0 3px 0 3px;
328
-}
329
-
330
-.videocontainer .editdisplayname {
331
-    height: auto;
332
-}
333
-
334
-#localDisplayName {
335
-    pointer-events: auto !important;
336
-}
337
-
338
-.videocontainer>a.displayname {
339
-    display: inline-block;
340
-    position: absolute;
341
-    color: #FFFFFF;
342
-    bottom: 0;
343
-    right: 0;
344
-    padding: 3px 5px;
345
-    font-size: 9pt;
346
-    cursor: pointer;
347
-    z-index: $zindex2;
348
-}
349
-
350
-/**
351
- * Video thumbnail toolbar icon.
352
- */
353
-.videocontainer .toolbar-icon {
354
-    font-size: 8pt;
355
-    text-align: center;
356
-    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
357
-    color: #FFFFFF;
358
-    width: 12px;
359
-    line-height: $thumbnailToolbarHeight;
360
-    height: $thumbnailToolbarHeight;
361
-    padding: 0;
362
-    border: 0;
363
-    margin: 0px 5px 0px 0px;
364
-}
365
-
366
-/**
367
- * Toolbar icon internal i elements (font icons).
368
- */
369
-.toolbar-icon>div {
370
-    height: $thumbnailToolbarHeight;
371
-    display: flex;
372
-    flex-direction: column;
373
-    justify-content: center;
374
-}
375
-
376
-/**
377
- * Toolbar icons positioned on the right.
378
- */
379
-.moderator-icon {
380
-    display: inline-block;
381
-
382
-    &.right {
383
-        float: right;
384
-        margin: 0px 0px 0px 5px;
385
-    }
386
-
387
-    .toolbar-icon {
388
-        margin: 0;
389
-    }
390
-}
391
-
392
-.raisehandindicator {
393
-  background: $raiseHandBg !important;
394
-}
395
-
396
-.connection-indicator {
397
-    background: $connectionIndicatorBg;
398
-
399
-    &.status-high {
400
-        background: green;
401
-    }
402
-
403
-    &.status-med {
404
-        background: #FFD740;
405
-    }
406
-
407
-    &.status-lost {
408
-        background: gray;
409
-    }
410
-
411
-    &.status-low {
412
-        background: #BF2117;
413
-    }
414
-
415
-    &.status-other {
416
-        background: $connectionIndicatorBg;
417
-    }
418
-
419
-    &.status-disabled {
420
-        background: transparent;
421
-        border: none
422
-    }
423
-}
424
-
425
-.local-video-menu-trigger,
426
-.remote-video-menu-trigger,
427
-.localvideomenu,
428
-.remotevideomenu
429
-{
430
-    display: inline-block;
431
-    position: absolute;
432
-    top: 0px;
433
-    right: 0;
434
-    z-index: $zindex2;
435
-    width: 18px;
436
-    height: 18px;
437
-    color: #FFF;
438
-    font-size: 10pt;
439
-    margin-right: $remoteVideoMenuIconMargin;
440
-
441
-    >i{
442
-        cursor: hand;
443
-    }
444
-}
445
-.local-video-menu-trigger,
446
-.remote-video-menu-trigger {
447
-    margin-top: 7px;
448
-}
449
-
450 142
 /**
451 143
  * Audio indicator on video thumbnails.
452 144
  */
@@ -623,74 +315,11 @@
623 315
     display: none;
624 316
 }
625 317
 
626
-.display-avatar-with-name {
627
-    .avatar-container {
628
-        visibility: visible;
629
-    }
630
-
631
-    .displayNameContainer {
632
-        visibility: visible;
633
-    }
634
-
635
-    .videocontainer__hoverOverlay {
636
-        visibility: visible;
637
-    }
638
-
639
-    video {
640
-        visibility: hidden;
641
-    }
642
-}
643
-
644
-.display-name-on-black {
645
-    .avatar-container {
646
-        visibility: hidden;
647
-    }
648
-
649
-    .displayNameContainer {
650
-        visibility: visible;
651
-    }
652
-
653
-    .videocontainer__hoverOverlay {
654
-        visibility: hidden;
655
-    }
656
-
657
-    video {
658
-        opacity: 0.2;
659
-        visibility: visible;
660
-    }
661
-}
662
-
663 318
 .display-video {
664 319
     .avatar-container {
665 320
         visibility: hidden;
666 321
     }
667 322
 
668
-    .displayNameContainer {
669
-        visibility: hidden;
670
-    }
671
-
672
-    .videocontainer__hoverOverlay {
673
-        visibility: hidden;
674
-    }
675
-
676
-    video {
677
-        visibility: visible;
678
-    }
679
-}
680
-
681
-.display-name-on-video {
682
-    .avatar-container {
683
-        visibility: hidden;
684
-    }
685
-
686
-    .displayNameContainer {
687
-        visibility: visible;
688
-    }
689
-
690
-    .videocontainer__hoverOverlay {
691
-        visibility: visible;
692
-    }
693
-
694 323
     video {
695 324
         visibility: visible;
696 325
     }
@@ -701,14 +330,6 @@
701 330
         visibility: visible;
702 331
     }
703 332
 
704
-    .displayNameContainer {
705
-        visibility: hidden;
706
-    }
707
-
708
-    .videocontainer__hoverOverlay {
709
-        visibility: hidden;
710
-    }
711
-
712 333
     video {
713 334
         visibility: hidden;
714 335
     }

+ 1
- 28
css/filmstrip/_small_video.scss Bestand weergeven

@@ -6,37 +6,10 @@
6 6
     border-radius: $borderRadius;
7 7
     margin: 0 $thumbnailVideoMargin;
8 8
 
9
-    &.videoContainerFocused, &:hover {
9
+    &:hover {
10 10
         cursor: hand;
11 11
     }
12 12
 
13
-    /**
14
-     * Focused video thumbnail.
15
-     */
16
-    &.videoContainerFocused {
17
-        border: $thumbnailVideoBorder solid $videoThumbnailSelected;
18
-        box-shadow: inset 0 0 3px $videoThumbnailSelected,
19
-        0 0 3px $videoThumbnailSelected;
20
-    }
21
-
22
-    .remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
23
-        display: none;
24
-    }
25
-
26
-    /**
27
-     * Hovered video thumbnail.
28
-     */
29
-    &:hover:not(.videoContainerFocused):not(.active-speaker) {
30
-        cursor: hand;
31
-        border: $thumbnailVideoBorder solid $videoThumbnailHovered;
32
-        box-shadow: inset 0 0 3px $videoThumbnailHovered,
33
-        0 0 3px $videoThumbnailHovered;
34
-
35
-        .remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
36
-            display: inline-block;
37
-        }
38
-    }
39
-
40 13
     & > video {
41 14
         cursor: hand;
42 15
         border-radius: $borderRadius;

+ 0
- 11
css/filmstrip/_tile_view.scss Bestand weergeven

@@ -2,13 +2,6 @@
2 2
  * CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
3 3
  */
4 4
 .tile-view {
5
-    /**
6
-     * Add a border around the active speaker to make the thumbnail easier to
7
-     * see.
8
-     */
9
-    .active-speaker {
10
-        box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
11
-    }
12 5
 
13 6
     .remote-videos {
14 7
         align-items: center;
@@ -134,7 +127,3 @@
134 127
         }
135 128
     }
136 129
 }
137
-
138
-.indicator-icon-container {
139
-    display: inline-block;
140
-}

+ 0
- 12
css/filmstrip/_tile_view_overrides.scss Bestand weergeven

@@ -35,16 +35,4 @@
35 35
     #remotePresenceMessage {
36 36
         display: none !important;
37 37
     }
38
-
39
-    /**
40
-     * Thumbnail popover menus can overlap other thumbnails. Setting an auto
41
-     * z-index will allow AtlasKit InlineDialog's large z-index to be
42
-     * respected and thereby display over elements in other thumbnails,
43
-     * specifically the various status icons.
44
-     */
45
-    .remotevideomenu,
46
-    .localvideomenu,
47
-    .videocontainer__toptoolbar {
48
-        z-index: auto;
49
-    }
50 38
 }

+ 0
- 66
css/filmstrip/_vertical_filmstrip_overrides.scss Bestand weergeven

@@ -19,72 +19,6 @@
19 19
  * Overrides for small videos in vertical filmstrip mode.
20 20
  */
21 21
 .vertical-filmstrip .filmstrip__videos .videocontainer {
22
-    /**
23
-     * Move status icons to the bottom right of the thumbnail.
24
-     */
25
-    .videocontainer__toolbar {
26
-        /**
27
-         * FIXME: disable pointer to allow any elements moved below to still
28
-         * be clickable. The real fix would to make sure those moved elements
29
-         * are actually part of the toolbar instead of positioning being faked.
30
-         */
31
-        pointer-events: none;
32
-        text-align: right;
33
-
34
-        > div {
35
-            pointer-events: none;
36
-        }
37
-
38
-        .right {
39
-            float: none;
40
-            margin: auto;
41
-        }
42
-
43
-        .toolbar-icon {
44
-            pointer-events: all;
45
-        }
46
-    }
47
-
48
-    /**
49
-     * Apply hardware acceleration to prevent flickering on scroll. The
50
-     * selectors are specific to icon wrappers to prevent fixed position dialogs
51
-     * and tooltips from getting a new location context due to translate3d.
52
-     */
53
-    .connection-indicator,
54
-    .local-video-menu-trigger,
55
-    .remote-video-menu-trigger,
56
-    .indicator-icon-container {
57
-        transform: translate3d(0, 0, 0);
58
-    }
59
-
60
-    .indicator-icon-container {
61
-        display: inline-block;
62
-    }
63
-
64
-    .indicator-container {
65
-        float: none;
66
-    }
67
-
68
-    /**
69
-     * Move the remote video menu trigger to the bottom left of the video
70
-     * thumbnail.
71
-     */
72
-    .localvideomenu,
73
-    .remotevideomenu,
74
-    .local-video-menu-trigger,
75
-    .remote-video-menu-trigger {
76
-        bottom: 0;
77
-        left: 0;
78
-        top: auto;
79
-        right: auto;
80
-    }
81
-
82
-    .local-video-menu-trigger,
83
-    .remote-video-menu-trigger {
84
-        margin-bottom: 3px;
85
-        margin-left: $remoteVideoMenuIconMargin;
86
-    }
87
-
88 22
     .self-view-mobile-portrait video {
89 23
         object-fit: contain;
90 24
     }

+ 0
- 4
css/themes/_light.scss Bestand weergeven

@@ -75,11 +75,7 @@ $errorColor: #c61600;
75 75
 $feedbackCancelFontColor: #333;
76 76
 
77 77
 // Popover colors
78
-$popoverBg: initial;
79 78
 $popoverFontColor: #ffffff !important;
80
-$popupMenuColor: #ffffff !important;
81
-$popupMenuHoverColor: #ffffff !important;
82
-$popupMenuHoverBackground: rgba(255, 255, 255, 0.1);
83 79
 $popupSliderColor: #0376da;
84 80
 
85 81
 // Toolbar

+ 1
- 1
interface_config.js Bestand weergeven

@@ -26,7 +26,7 @@ var interfaceConfig = {
26 26
 
27 27
     CLOSE_PAGE_GUEST_HINT: false, // A html text to be shown to guests on the close page, false disables it
28 28
 
29
-    DEFAULT_BACKGROUND: '#474747',
29
+    DEFAULT_BACKGROUND: '#040404',
30 30
     DEFAULT_LOGO_URL: 'images/watermark.svg',
31 31
     DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.svg',
32 32
 

+ 2
- 2
lang/main.json Bestand weergeven

@@ -949,8 +949,8 @@
949 949
             "mute": "Mute / Unmute",
950 950
             "muteEveryone": "Mute everyone",
951 951
             "muteEveryoneElse": "Mute everyone else",
952
-            "muteEveryonesVideo": "Disable everyone's video",
953
-            "muteEveryoneElsesVideo": "Disable everyone else's video",
952
+            "muteEveryonesVideoStream": "Stop everyone's video",
953
+            "muteEveryoneElsesVideoStream": "Stop everyone else's video",
954 954
             "participants": "Participants",
955 955
             "pip": "Toggle Picture-in-Picture mode",
956 956
             "privateMessage": "Send private message",

+ 18
- 4
react/features/base/components/context-menu/ContextMenu.js Bestand weergeven

@@ -19,7 +19,7 @@ type Props = {
19 19
     /**
20 20
      * Class name for context menu. Used to overwrite default styles.
21 21
      */
22
-    className?: string,
22
+    className?: ?string,
23 23
 
24 24
     /**
25 25
      * The entity for which the context menu is displayed.
@@ -31,10 +31,15 @@ type Props = {
31 31
      */
32 32
     hidden?: boolean,
33 33
 
34
+    /**
35
+     * Whether or not the menu is already in a drawer.
36
+     */
37
+    inDrawer?: ?boolean,
38
+
34 39
     /**
35 40
      * Whether or not drawer should be open.
36 41
      */
37
-    isDrawerOpen: boolean,
42
+    isDrawerOpen?: boolean,
38 43
 
39 44
     /**
40 45
      * Target elements against which positioning calculations are made.
@@ -49,7 +54,7 @@ type Props = {
49 54
     /**
50 55
      * Callback for drawer close.
51 56
      */
52
-    onDrawerClose: Function,
57
+    onDrawerClose?: Function,
53 58
 
54 59
     /**
55 60
      * Callback for the mouse entering the component.
@@ -59,7 +64,7 @@ type Props = {
59 64
     /**
60 65
      * Callback for the mouse leaving the component.
61 66
      */
62
-    onMouseLeave: Function
67
+    onMouseLeave?: Function
63 68
 };
64 69
 
65 70
 const useStyles = makeStyles(theme => {
@@ -106,6 +111,7 @@ const ContextMenu = ({
106 111
     className,
107 112
     entity,
108 113
     hidden,
114
+    inDrawer,
109 115
     isDrawerOpen,
110 116
     offsetTarget,
111 117
     onClick,
@@ -147,6 +153,14 @@ const ContextMenu = ({
147 153
         }
148 154
     }, [ hidden ]);
149 155
 
156
+    if (_overflowDrawer && inDrawer) {
157
+        return (<div
158
+            className = { styles.drawer }
159
+            onClick = { onDrawerClose }>
160
+            {children}
161
+        </div>);
162
+    }
163
+
150 164
     return _overflowDrawer
151 165
         ? <JitsiPortal>
152 166
             <Drawer

+ 129
- 0
react/features/base/components/context-menu/ContextMenuItem.js Bestand weergeven

@@ -0,0 +1,129 @@
1
+// @flow
2
+
3
+import { makeStyles } from '@material-ui/styles';
4
+import clsx from 'clsx';
5
+import React from 'react';
6
+import { useSelector } from 'react-redux';
7
+
8
+import { showOverflowDrawer } from '../../../toolbox/functions.web';
9
+import { Icon } from '../../icons';
10
+
11
+export type Props = {
12
+
13
+    /**
14
+     * Label used for accessibility.
15
+     */
16
+    accessibilityLabel: string,
17
+
18
+    /**
19
+     * CSS class name used for custom styles.
20
+     */
21
+    className?: string,
22
+
23
+    /**
24
+     * Custom icon. If used, the icon prop is ignored.
25
+     * Used to allow custom children instead of just the default icons.
26
+     */
27
+    customIcon?: React$Node,
28
+
29
+    /**
30
+     * Whether or not the action is disabled.
31
+     */
32
+    disabled?: boolean,
33
+
34
+    /**
35
+     * Id of the action container.
36
+     */
37
+    id?: string,
38
+
39
+    /**
40
+     * Default icon for action.
41
+     */
42
+    icon?: Function,
43
+
44
+    /**
45
+     * Click handler.
46
+     */
47
+    onClick?: Function,
48
+
49
+    /**
50
+     * Action text.
51
+     */
52
+    text: string,
53
+
54
+    /**
55
+     * Class name for the text.
56
+     */
57
+    textClassName?: string
58
+}
59
+
60
+const useStyles = makeStyles(theme => {
61
+    return {
62
+        contextMenuItem: {
63
+            alignItems: 'center',
64
+            cursor: 'pointer',
65
+            display: 'flex',
66
+            minHeight: '40px',
67
+            padding: '10px 16px',
68
+            boxSizing: 'border-box',
69
+
70
+            '& > *:not(:last-child)': {
71
+                marginRight: `${theme.spacing(3)}px`
72
+            },
73
+
74
+            '&:hover': {
75
+                backgroundColor: theme.palette.ui04
76
+            }
77
+        },
78
+
79
+        contextMenuItemDisabled: {
80
+            pointerEvents: 'none'
81
+        },
82
+
83
+        contextMenuItemDrawer: {
84
+            padding: '12px 16px'
85
+        },
86
+
87
+        contextMenuItemIcon: {
88
+            '& svg': {
89
+                fill: theme.palette.icon01
90
+            }
91
+        }
92
+    };
93
+});
94
+
95
+const ContextMenuItem = ({
96
+    accessibilityLabel,
97
+    className,
98
+    customIcon,
99
+    disabled,
100
+    id,
101
+    icon,
102
+    onClick,
103
+    text,
104
+    textClassName }: Props) => {
105
+    const styles = useStyles();
106
+    const _overflowDrawer = useSelector(showOverflowDrawer);
107
+
108
+    return (
109
+        <div
110
+            aria-label = { accessibilityLabel }
111
+            className = { clsx(styles.contextMenuItem,
112
+                    _overflowDrawer && styles.contextMenuItemDrawer,
113
+                    disabled && styles.contextMenuItemDisabled,
114
+                    className
115
+            ) }
116
+            id = { id }
117
+            key = { text }
118
+            onClick = { onClick }>
119
+            {customIcon ? customIcon
120
+                : icon && <Icon
121
+                    className = { styles.contextMenuItemIcon }
122
+                    size = { 20 }
123
+                    src = { icon } />}
124
+            <span className = { textClassName ?? '' }>{text}</span>
125
+        </div>
126
+    );
127
+};
128
+
129
+export default ContextMenuItem;

+ 5
- 89
react/features/base/components/context-menu/ContextMenuItemGroup.js Bestand weergeven

@@ -1,50 +1,8 @@
1 1
 // @flow
2 2
 import { makeStyles } from '@material-ui/core';
3
-import clsx from 'clsx';
4 3
 import React from 'react';
5
-import { useSelector } from 'react-redux';
6 4
 
7
-import { showOverflowDrawer } from '../../../toolbox/functions.web';
8
-import { Icon } from '../../icons';
9
-
10
-export type Action = {
11
-
12
-    /**
13
-     * Label used for accessibility.
14
-     */
15
-    accessibilityLabel: string,
16
-
17
-    /**
18
-     * CSS class name used for custom styles.
19
-     */
20
-    className?: string,
21
-
22
-    /**
23
-     * Custom icon. If used, the icon prop is ignored.
24
-     * Used to allow custom children instead of just the default icons.
25
-     */
26
-    customIcon?: React$Node,
27
-
28
-    /**
29
-     * Id of the action container.
30
-     */
31
-    id?: string,
32
-
33
-    /**
34
-     * Default icon for action.
35
-     */
36
-    icon?: Function,
37
-
38
-    /**
39
-     * Click handler.
40
-     */
41
-    onClick?: Function,
42
-
43
-    /**
44
-     * Action text.
45
-     */
46
-    text: string
47
-}
5
+import ContextMenuItem, { type Props as Action } from './ContextMenuItem';
48 6
 
49 7
 type Props = {
50 8
 
@@ -59,7 +17,6 @@ type Props = {
59 17
     children?: React$Node,
60 18
 };
61 19
 
62
-
63 20
 const useStyles = makeStyles(theme => {
64 21
     return {
65 22
         contextMenuItemGroup: {
@@ -70,33 +27,6 @@ const useStyles = makeStyles(theme => {
70 27
             '& + &:not(:empty)': {
71 28
                 borderTop: `1px solid ${theme.palette.ui04}`
72 29
             }
73
-        },
74
-
75
-        contextMenuItem: {
76
-            alignItems: 'center',
77
-            cursor: 'pointer',
78
-            display: 'flex',
79
-            minHeight: '40px',
80
-            padding: '10px 16px',
81
-            boxSizing: 'border-box',
82
-
83
-            '& > *:not(:last-child)': {
84
-                marginRight: `${theme.spacing(3)}px`
85
-            },
86
-
87
-            '&:hover': {
88
-                backgroundColor: theme.palette.ui04
89
-            }
90
-        },
91
-
92
-        contextMenuItemDrawer: {
93
-            padding: '12px 16px'
94
-        },
95
-
96
-        contextMenuItemIcon: {
97
-            '& svg': {
98
-                fill: theme.palette.icon01
99
-            }
100 30
         }
101 31
     };
102 32
 });
@@ -106,28 +36,14 @@ const ContextMenuItemGroup = ({
106 36
     children
107 37
 }: Props) => {
108 38
     const styles = useStyles();
109
-    const _overflowDrawer = useSelector(showOverflowDrawer);
110 39
 
111 40
     return (
112 41
         <div className = { styles.contextMenuItemGroup }>
113 42
             {children}
114
-            {actions && actions.map(({ accessibilityLabel, className, customIcon, id, icon, onClick, text }) => (
115
-                <div
116
-                    aria-label = { accessibilityLabel }
117
-                    className = { clsx(styles.contextMenuItem,
118
-                        _overflowDrawer && styles.contextMenuItemDrawer,
119
-                        className
120
-                    ) }
121
-                    id = { id }
122
-                    key = { text }
123
-                    onClick = { onClick }>
124
-                    {customIcon ? customIcon
125
-                        : icon && <Icon
126
-                            className = { styles.contextMenuItemIcon }
127
-                            size = { 20 }
128
-                            src = { icon } />}
129
-                    <span>{text}</span>
130
-                </div>
43
+            {actions && actions.map(actionProps => (
44
+                <ContextMenuItem
45
+                    key = { actionProps.text }
46
+                    { ...actionProps } />
131 47
             ))}
132 48
         </div>
133 49
     );

+ 2
- 2
react/features/base/icons/svg/crown.svg Bestand weergeven

@@ -1,3 +1,3 @@
1
-<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
-<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4C14 4.85739 13.4605 5.58876 12.7024 5.87317L14.2286 9.94296L14.9455 11.8546C15.0074 11.9292 15.0708 11.9292 15.1098 11.8902L16.5535 10.4465L18.5858 8.41421C18.2239 8.05228 18 7.55228 18 7C18 5.89543 18.8954 5 20 5C21.1046 5 22 5.89543 22 7C22 8.10457 21.1046 9 20 9C19.9441 9 19.8887 8.9977 19.8339 8.9932L19 19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19L4.1661 8.9932C4.11133 8.9977 4.05593 9 4 9C2.89543 9 2 8.10457 2 7C2 5.89543 2.89543 5 4 5C5.10457 5 6 5.89543 6 7C6 7.55228 5.77614 8.05228 5.41421 8.41421L7.44654 10.4465L8.89019 11.8902C8.9775 11.9325 9.03514 11.9063 9.05453 11.8546L9.77139 9.94296L11.2976 5.87317C10.5395 5.58876 10 4.85739 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4ZM6.84027 17L6.44651 12.2749L7.47597 13.3044C7.68795 13.5164 7.94285 13.6805 8.22354 13.7858C9.30949 14.193 10.52 13.6428 10.9272 12.5568L12 9.696L13.0728 12.5568C13.1781 12.8375 13.3422 13.0924 13.5542 13.3044C14.3743 14.1245 15.7039 14.1245 16.524 13.3044L17.5535 12.2749L17.1597 17H6.84027Z"/>
1
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 2.5C8.75 3.03587 8.41281 3.49298 7.93902 3.67073L8.89288 6.21435L9.34092 7.40912C9.37965 7.45577 9.41923 7.45577 9.44363 7.43137L10.3459 6.52909L11.6161 5.25888C11.3899 5.03268 11.25 4.72018 11.25 4.375C11.25 3.68464 11.8096 3.125 12.5 3.125C13.1904 3.125 13.75 3.68464 13.75 4.375C13.75 5.06536 13.1904 5.625 12.5 5.625C12.465 5.625 12.4304 5.62356 12.3962 5.62075L11.875 11.875C11.875 12.5654 11.3154 13.125 10.625 13.125H4.375C3.68464 13.125 3.125 12.5654 3.125 11.875L2.60381 5.62075C2.56958 5.62356 2.53496 5.625 2.5 5.625C1.80964 5.625 1.25 5.06536 1.25 4.375C1.25 3.68464 1.80964 3.125 2.5 3.125C3.19036 3.125 3.75 3.68464 3.75 4.375C3.75 4.72018 3.61009 5.03268 3.38388 5.25888L4.65409 6.52909L5.55637 7.43137C5.61094 7.45781 5.64696 7.44144 5.65908 7.40912L6.10712 6.21435L7.06098 3.67073C6.58719 3.49298 6.25 3.03587 6.25 2.5C6.25 1.80964 6.80964 1.25 7.5 1.25C8.19036 1.25 8.75 1.80964 8.75 2.5ZM4.27517 10.625L4.02907 7.67184L4.67248 8.31525C4.80497 8.44773 4.96428 8.55032 5.13971 8.6161C5.81843 8.87063 6.57497 8.52674 6.82949 7.84802L7.5 6.06L8.17051 7.84802C8.23629 8.02345 8.33888 8.18277 8.47136 8.31525C8.98392 8.82781 9.81495 8.82781 10.3275 8.31525L10.9709 7.67184L10.7248 10.625H4.27517Z" fill="white"/>
3 3
 </svg>

+ 3
- 10
react/features/base/icons/svg/mute-everyone-else.svg Bestand weergeven

@@ -1,11 +1,4 @@
1
-<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
-<g clip-path="url(#clip0)">
3
-<path fill-rule="evenodd" clip-rule="evenodd" d="M6 13.078V15C6 16.3999 6.9589 17.5759 8.25572 17.907C8.25195 17.9374 8.25 17.9685 8.25 18V19.4378C6.12171 19.0807 4.5 17.2297 4.5 15C4.5 14.5858 4.16421 14.25 3.75 14.25C3.33579 14.25 3 14.5858 3 15C3 18.0597 5.29027 20.5845 8.25 20.9536V21.75C8.25 22.1642 8.58579 22.5 9 22.5C9.41421 22.5 9.75 22.1642 9.75 21.75V20.9536C10.8412 20.8175 11.8415 20.3884 12.6694 19.7475L15.1986 22.2766C15.4964 22.5744 15.9791 22.5745 16.2768 22.2768C16.5745 21.9791 16.5744 21.4964 16.2766 21.1986L13.7475 18.6694C13.7502 18.6659 13.753 18.6623 13.7557 18.6588L12.6831 17.5861C12.6805 17.5898 12.6779 17.5935 12.6753 17.5972L11.5911 16.513C11.5934 16.5091 11.5957 16.5051 11.598 16.5011L10.4566 15.3596C10.4554 15.3647 10.4541 15.3697 10.4528 15.3748L7.5 12.422V12.403L6 10.903V10.922L2.80143 7.72339C2.50364 7.4256 2.02091 7.42553 1.72322 7.72322C1.42553 8.02091 1.4256 8.50364 1.72339 8.80143L6 13.078ZM7.5 14.578V15C7.5 15.8284 8.17157 16.5 9 16.5C9.1294 16.5 9.25498 16.4836 9.37476 16.4528L7.5 14.578ZM10.513 17.5911C10.2756 17.73 10.0175 17.8372 9.74428 17.907C9.74805 17.9374 9.75 17.9685 9.75 18V19.4378C10.4295 19.3238 11.0573 19.0575 11.5972 18.6753L10.513 17.5911ZM12 14.747L10.5 13.247V10.5C10.5 9.67157 9.82843 9 9 9C8.25144 9 7.63095 9.54832 7.51827 10.2652L6.34845 9.09541C6.85223 8.14635 7.85064 7.5 9 7.5C10.6569 7.5 12 8.84315 12 10.5V14.747ZM13.3623 16.1092L14.5462 17.2932C14.8386 16.5867 15 15.8122 15 15C15 14.5858 14.6642 14.25 14.25 14.25C13.8358 14.25 13.5 14.5858 13.5 15C13.5 15.3828 13.4522 15.7544 13.3623 16.1092Z" />
4
-<path fill-rule="evenodd" clip-rule="evenodd" d="M16 4.71869V6C16 6.93329 16.6393 7.71727 17.5038 7.93797C17.5013 7.95829 17.5 7.97899 17.5 8V8.95852C16.0811 8.72048 15 7.4865 15 6C15 5.72386 14.7761 5.5 14.5 5.5C14.2239 5.5 14 5.72386 14 6C14 8.03981 15.5268 9.723 17.5 9.96905V10.5C17.5 10.7761 17.7239 11 18 11C18.2761 11 18.5 10.7761 18.5 10.5V9.96905C19.2275 9.87834 19.8943 9.59227 20.4463 9.16499L22.1324 10.8511C22.3309 11.0496 22.6527 11.0496 22.8512 10.8512C23.0496 10.6527 23.0496 10.3309 22.8511 10.1324L21.165 8.4463C21.1668 8.44393 21.1687 8.44155 21.1705 8.43918L20.4554 7.7241C20.4537 7.72656 20.4519 7.72903 20.4502 7.73149L19.7274 7.00869C19.7289 7.00603 19.7305 7.00338 19.732 7.00072L18.9711 6.23977C18.9702 6.24313 18.9694 6.24649 18.9685 6.24984L17 4.28131V4.26869L16 3.26869V3.28131L13.8676 1.14893C13.6691 0.950402 13.3473 0.950351 13.1488 1.14881C12.9504 1.34727 12.9504 1.6691 13.1489 1.86762L16 4.71869ZM17 5.71869V6C17 6.55228 17.4477 7 18 7C18.0863 7 18.17 6.98908 18.2498 6.96854L17 5.71869ZM19.0087 7.72738C18.8504 7.81999 18.6783 7.89148 18.4962 7.93797C18.4987 7.95829 18.5 7.97899 18.5 8V8.95852C18.953 8.88252 19.3715 8.70502 19.7315 8.45019L19.0087 7.72738ZM20 5.83131L19 4.83131V3C19 2.44772 18.5523 2 18 2C17.501 2 17.0873 2.36555 17.0122 2.84348L16.2323 2.06361C16.5682 1.4309 17.2338 1 18 1C19.1046 1 20 1.89543 20 3V5.83131ZM20.9082 6.73948L21.6975 7.52877C21.8924 7.05778 22 6.54145 22 6C22 5.72386 21.7761 5.5 21.5 5.5C21.2239 5.5 21 5.72386 21 6C21 6.25519 20.9681 6.50294 20.9082 6.73948Z" />
5
-</g>
6
-<defs>
7
-<clipPath id="clip0">
8
-<rect width="24" height="24"/>
9
-</clipPath>
10
-</defs>
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5 10.8984V12.5C5 13.6666 5.79908 14.6466 6.87977 14.9225C6.87662 14.9479 6.875 14.9737 6.875 15V16.1982C5.10143 15.9006 3.75 14.3581 3.75 12.5C3.75 12.1548 3.47018 11.875 3.125 11.875C2.77982 11.875 2.5 12.1548 2.5 12.5C2.5 15.0498 4.40856 17.1538 6.875 17.4613V18.125C6.875 18.4702 7.15482 18.75 7.5 18.75C7.84518 18.75 8.125 18.4702 8.125 18.125V17.4613C9.03436 17.3479 9.86788 16.9903 10.5579 16.4562L12.6655 18.5638C12.9136 18.812 13.3159 18.8121 13.564 18.564C13.8121 18.3159 13.812 17.9136 13.5638 17.6655L11.4562 15.5579C11.4585 15.5549 11.4608 15.5519 11.4631 15.549L10.5693 14.6551C10.5671 14.6582 10.5649 14.6613 10.5627 14.6644L9.65923 13.7609C9.66117 13.7575 9.6631 13.7542 9.66503 13.7509L8.71384 12.7997C8.7128 12.8039 8.71175 12.8081 8.71067 12.8123L6.25 10.3516V10.3359L5 9.08587V9.10163L2.33453 6.43616C2.08637 6.188 1.68409 6.18794 1.43602 6.43602C1.18794 6.68409 1.188 7.08637 1.43616 7.33453L5 10.8984ZM6.25 12.1484V12.5C6.25 13.1904 6.80964 13.75 7.5 13.75C7.60783 13.75 7.71248 13.7363 7.8123 13.7107L6.25 12.1484ZM8.76086 14.6592C8.56304 14.775 8.34788 14.8643 8.12023 14.9225C8.12338 14.9479 8.125 14.9737 8.125 15V16.1982C8.69123 16.1032 9.21443 15.8813 9.66436 15.5627L8.76086 14.6592ZM10 12.2891L8.75 11.0391V8.75C8.75 8.05964 8.19036 7.5 7.5 7.5C6.8762 7.5 6.35913 7.95693 6.26522 8.55435L5.29038 7.57951C5.71019 6.78863 6.5422 6.25 7.5 6.25C8.88071 6.25 10 7.36929 10 8.75V12.2891ZM11.1352 13.4243L12.1218 14.411C12.3655 13.8222 12.5 13.1768 12.5 12.5C12.5 12.1548 12.2202 11.875 11.875 11.875C11.5298 11.875 11.25 12.1548 11.25 12.5C11.25 12.819 11.2102 13.1287 11.1352 13.4243Z" fill="white"/>
3
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 3.93224V5C13.3333 5.77774 13.8661 6.43106 14.5865 6.61497C14.5844 6.63191 14.5833 6.64916 14.5833 6.66666V7.46543C13.401 7.26706 12.5 6.23874 12.5 4.99999C12.5 4.76988 12.3135 4.58333 12.0833 4.58333C11.8532 4.58333 11.6667 4.76988 11.6667 4.99999C11.6667 6.69984 12.939 8.1025 14.5833 8.30754V8.74999C14.5833 8.98011 14.7699 9.16666 15 9.16666C15.2301 9.16666 15.4167 8.98011 15.4167 8.74999V8.30754C16.0229 8.23194 16.5786 7.99355 17.0386 7.63749L18.4437 9.04256C18.6091 9.20799 18.8773 9.20804 19.0427 9.04265C19.2081 8.87727 19.208 8.60908 19.0426 8.44364L17.6375 7.03857C17.639 7.0366 17.6406 7.03462 17.6421 7.03264L17.0462 6.43674C17.0447 6.4388 17.0433 6.44085 17.0418 6.4429L16.4395 5.84057C16.4408 5.83836 16.4421 5.83614 16.4434 5.83392L15.8092 5.1998C15.8085 5.2026 15.8078 5.2054 15.8071 5.2082L14.1667 3.56775V3.55724L13.3333 2.72391V2.73442L11.5564 0.957435C11.3909 0.791997 11.1227 0.791954 10.9574 0.957339C10.792 1.12272 10.792 1.39091 10.9574 1.55635L13.3333 3.93224ZM14.1667 4.76557V4.99999C14.1667 5.46023 14.5398 5.83333 15 5.83333C15.0719 5.83333 15.1417 5.82422 15.2082 5.80711L14.1667 4.76557ZM15.8406 6.43948C15.7087 6.51666 15.5653 6.57623 15.4135 6.61497C15.4156 6.63191 15.4167 6.64916 15.4167 6.66666V7.46543C15.7942 7.4021 16.143 7.25417 16.4429 7.04182L15.8406 6.43948ZM16.6667 4.85942L15.8333 4.02608V2.49999C15.8333 2.03976 15.4602 1.66666 15 1.66666C14.5841 1.66666 14.2394 1.97128 14.1768 2.36956L13.5269 1.71967C13.8068 1.19241 14.3615 0.833328 15 0.833328C15.9205 0.833328 16.6667 1.57952 16.6667 2.49999V4.85942ZM17.4235 5.61623L18.0812 6.27397C18.2437 5.88148 18.3333 5.45121 18.3333 4.99999C18.3333 4.76988 18.1468 4.58333 17.9167 4.58333C17.6866 4.58333 17.5 4.76988 17.5 4.99999C17.5 5.21265 17.4735 5.41911 17.4235 5.61623Z" fill="white"/>
11 4
 </svg>

+ 2
- 2
react/features/base/icons/svg/share-desktop.svg Bestand weergeven

@@ -1,3 +1,3 @@
1
-<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
2
-<path fill-rule="evenodd" clip-rule="evenodd" d="M3.66671 2.75H18.3334C19.3459 2.75 20.1667 3.57081 20.1667 4.58333V15.5833C20.1667 16.5959 19.3459 17.4167 18.3334 17.4167H15.5834C16.0896 17.4167 16.5 17.8271 16.5 18.3333C16.5 18.8396 16.0896 19.25 15.5834 19.25H6.41671C5.91045 19.25 5.50004 18.8396 5.50004 18.3333C5.50004 17.8271 5.91045 17.4167 6.41671 17.4167H3.66671C2.65419 17.4167 1.83337 16.5959 1.83337 15.5833V4.58333C1.83337 3.57081 2.65419 2.75 3.66671 2.75ZM3.66671 4.58333V15.5833H18.3334V4.58333H3.66671ZM11.9167 8.25C8.16671 8.25 6.41671 9.85417 6.41671 14.6667C8.41671 10.7708 11.9167 11 11.9167 11V12.274C11.9167 12.6941 12.4034 12.9269 12.7305 12.6633L16.017 10.0143C16.2654 9.81413 16.2654 9.43582 16.017 9.23568L12.7305 6.5867C12.4034 6.32307 11.9167 6.55589 11.9167 6.97599V8.25Z" />
1
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 1.875H12.5C13.1904 1.875 13.75 2.43464 13.75 3.125V10.625C13.75 11.3154 13.1904 11.875 12.5 11.875H10.625C10.9702 11.875 11.25 12.1548 11.25 12.5C11.25 12.8452 10.9702 13.125 10.625 13.125H4.375C4.02982 13.125 3.75 12.8452 3.75 12.5C3.75 12.1548 4.02982 11.875 4.375 11.875H2.5C1.80964 11.875 1.25 11.3154 1.25 10.625V3.125C1.25 2.43464 1.80964 1.875 2.5 1.875ZM2.5 3.125V10.625H12.5V3.125H2.5ZM8.125 5.625C5.56818 5.625 4.375 6.71875 4.375 10C5.73864 7.34375 8.125 7.5 8.125 7.5V8.03605C8.125 8.45615 8.61169 8.68897 8.93877 8.42534L10.767 6.95178C11.0153 6.75164 11.0153 6.37333 10.767 6.17319L8.93877 4.69963C8.61169 4.436 8.125 4.66882 8.125 5.08892V5.625Z" fill="white"/>
3 3
 </svg>

+ 3
- 13
react/features/base/popover/components/Popover.web.js Bestand weergeven

@@ -1,5 +1,4 @@
1 1
 /* @flow */
2
-import clsx from 'clsx';
3 2
 import React, { Component } from 'react';
4 3
 
5 4
 import { Drawer, JitsiPortal, DialogPortal } from '../../../toolbox/components/web';
@@ -60,11 +59,6 @@ type Props = {
60 59
      */
61 60
     position: string,
62 61
 
63
-    /**
64
-     * Whether the content show have some padding.
65
-     */
66
-    paddedContent: ?boolean,
67
-
68 62
     /**
69 63
      * Whether the popover is visible or not.
70 64
      */
@@ -79,7 +73,7 @@ type State = {
79 73
     /**
80 74
      * The style to apply to the context menu in order to position it correctly.
81 75
      */
82
-     contextMenuStyle: Object
76
+    contextMenuStyle: Object
83 77
 };
84 78
 
85 79
 /**
@@ -364,15 +358,11 @@ class Popover extends Component<Props, State> {
364 358
      * @returns {ReactElement}
365 359
      */
366 360
     _renderContent() {
367
-        const { content, paddedContent } = this.props;
368
-        const className = clsx(
369
-            'popover popupmenu',
370
-            paddedContent && 'padded-content'
371
-        );
361
+        const { content } = this.props;
372 362
 
373 363
         return (
374 364
             <div
375
-                className = { className }
365
+                className = 'popover'
376 366
                 onKeyDown = { this._onEscKey }>
377 367
                 { content }
378 368
                 {!isMobileBrowser() && (

+ 51
- 58
react/features/base/react/components/web/BaseIndicator.js Bestand weergeven

@@ -1,6 +1,7 @@
1 1
 /* @flow */
2 2
 
3
-import React, { Component } from 'react';
3
+import { makeStyles } from '@material-ui/core';
4
+import React from 'react';
4 5
 
5 6
 import { translate } from '../../../i18n';
6 7
 import { Icon } from '../../../icons';
@@ -12,7 +13,7 @@ import { Tooltip } from '../../../tooltip';
12 13
 type Props = {
13 14
 
14 15
     /**
15
-     * Additional CSS class names to set on the icon container.
16
+     * Additional CSS class name.
16 17
      */
17 18
     className: string,
18 19
 
@@ -59,66 +60,58 @@ type Props = {
59 60
     tooltipPosition: string
60 61
 };
61 62
 
63
+const useStyles = makeStyles(() => {
64
+    return {
65
+        indicator: {
66
+            width: '20px',
67
+            height: '20px',
68
+            display: 'flex',
69
+            alignItems: 'center',
70
+            justifyContent: 'center'
71
+        }
72
+    };
73
+});
74
+
62 75
 /**
63 76
  * React {@code Component} for showing an icon with a tooltip.
64 77
  *
65
- * @augments Component
78
+ * @returns {ReactElement}
66 79
  */
67
-class BaseIndicator extends Component<Props> {
68
-    /**
69
-     * Default values for {@code BaseIndicator} component's properties.
70
-     *
71
-     * @static
72
-     */
73
-    static defaultProps = {
74
-        className: '',
75
-        id: '',
76
-        tooltipPosition: 'top'
77
-    };
78
-
79
-    /**
80
-     * Implements React's {@link Component#render()}.
81
-     *
82
-     * @inheritdoc
83
-     * @returns {ReactElement}
84
-     */
85
-    render() {
86
-        const {
87
-            className,
88
-            icon,
89
-            iconClassName,
90
-            iconId,
91
-            iconSize,
92
-            id,
93
-            t,
94
-            tooltipKey,
95
-            tooltipPosition
96
-        } = this.props;
97
-        const iconContainerClassName = `indicator-icon-container ${className}`;
98
-        const style = {};
99
-
100
-        if (iconSize) {
101
-            style.fontSize = iconSize;
102
-        }
103
-
104
-        return (
105
-            <div className = 'indicator-container'>
106
-                <Tooltip
107
-                    content = { t(tooltipKey) }
108
-                    position = { tooltipPosition }>
109
-                    <span
110
-                        className = { iconContainerClassName }
111
-                        id = { id }>
112
-                        <Icon
113
-                            className = { iconClassName }
114
-                            id = { iconId }
115
-                            src = { icon }
116
-                            style = { style } />
117
-                    </span>
118
-                </Tooltip>
119
-            </div>
120
-        );
80
+const BaseIndicator = ({
81
+    className = '',
82
+    icon,
83
+    iconClassName,
84
+    iconId,
85
+    iconSize,
86
+    id = '',
87
+    t,
88
+    tooltipKey,
89
+    tooltipPosition = 'top'
90
+}: Props) => {
91
+    const styles = useStyles();
92
+    const style = {};
93
+
94
+    if (iconSize) {
95
+        style.fontSize = iconSize;
121 96
     }
122
-}
97
+
98
+    return (
99
+        <div className = { styles.indicator }>
100
+            <Tooltip
101
+                content = { t(tooltipKey) }
102
+                position = { tooltipPosition }>
103
+                <span
104
+                    className = { className }
105
+                    id = { id }>
106
+                    <Icon
107
+                        className = { iconClassName }
108
+                        id = { iconId }
109
+                        src = { icon }
110
+                        style = { style } />
111
+                </span>
112
+            </Tooltip>
113
+        </div>
114
+    );
115
+};
123 116
 
124 117
 export default translate(BaseIndicator);

+ 74
- 51
react/features/connection-indicator/components/web/ConnectionIndicator.js Bestand weergeven

@@ -1,5 +1,7 @@
1 1
 // @flow
2 2
 
3
+import { withStyles } from '@material-ui/styles';
4
+import clsx from 'clsx';
3 5
 import React from 'react';
4 6
 import type { Dispatch } from 'redux';
5 7
 
@@ -30,24 +32,21 @@ const QUALITY_TO_WIDTH: Array<Object> = [
30 32
     {
31 33
         colorClass: 'status-high',
32 34
         percent: INDICATOR_DISPLAY_THRESHOLD,
33
-        tip: 'connectionindicator.quality.good',
34
-        width: '100%'
35
+        tip: 'connectionindicator.quality.good'
35 36
     },
36 37
 
37 38
     // 2 bars
38 39
     {
39 40
         colorClass: 'status-med',
40 41
         percent: 10,
41
-        tip: 'connectionindicator.quality.nonoptimal',
42
-        width: '66%'
42
+        tip: 'connectionindicator.quality.nonoptimal'
43 43
     },
44 44
 
45 45
     // 1 bar
46 46
     {
47 47
         colorClass: 'status-low',
48 48
         percent: 0,
49
-        tip: 'connectionindicator.quality.poor',
50
-        width: '33%'
49
+        tip: 'connectionindicator.quality.poor'
51 50
     }
52 51
 
53 52
     // Note: we never show 0 bars as long as there is a connection.
@@ -85,6 +84,11 @@ type Props = AbstractProps & {
85 84
      */
86 85
     audioSsrc: number,
87 86
 
87
+    /**
88
+     * An object containing the CSS classes.
89
+     */
90
+    classes: Object,
91
+
88 92
     /**
89 93
      * The Redux dispatch function.
90 94
      */
@@ -122,6 +126,52 @@ type State = AbstractState & {
122 126
     popoverVisible: boolean
123 127
 }
124 128
 
129
+const styles = theme => {
130
+    return {
131
+        container: {
132
+            display: 'inline-block'
133
+        },
134
+
135
+        hidden: {
136
+            display: 'none'
137
+        },
138
+
139
+        icon: {
140
+            padding: '6px',
141
+            borderRadius: '4px',
142
+
143
+            '&.status-high': {
144
+                backgroundColor: theme.palette.success01
145
+            },
146
+
147
+            '&.status-med': {
148
+                backgroundColor: theme.palette.warning01
149
+            },
150
+
151
+            '&.status-low': {
152
+                backgroundColor: theme.palette.iconError
153
+            },
154
+
155
+            '&.status-disabled': {
156
+                background: 'transparent'
157
+            },
158
+
159
+            '&.status-lost': {
160
+                backgroundColor: theme.palette.ui05
161
+            },
162
+
163
+            '&.status-other': {
164
+                backgroundColor: theme.palette.action01
165
+            }
166
+        },
167
+
168
+        inactiveIcon: {
169
+            padding: 0,
170
+            borderRadius: '50%'
171
+        }
172
+    };
173
+};
174
+
125 175
 /**
126 176
  * Implements a React {@link Component} which displays the current connection
127 177
  * quality percentage and has a popover to show more detailed connection stats.
@@ -154,9 +204,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
154 204
      * @returns {ReactElement}
155 205
      */
156 206
     render() {
157
-        const { enableStatsDisplay, participantId, statsPopoverPosition } = this.props;
207
+        const { enableStatsDisplay, participantId, statsPopoverPosition, classes } = this.props;
158 208
         const visibilityClass = this._getVisibilityClass();
159
-        const rootClassNames = `indicator-container ${visibilityClass}`;
160 209
 
161 210
         if (this.props._popoverDisabled) {
162 211
             return this._renderIndicator();
@@ -164,7 +213,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
164 213
 
165 214
         return (
166 215
             <Popover
167
-                className = { rootClassNames }
216
+                className = { clsx(classes.container, visibilityClass) }
168 217
                 content = { <ConnectionIndicatorContent
169 218
                     inheritedStats = { this.state.stats }
170 219
                     participantId = { participantId } /> }
@@ -173,7 +222,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
173 222
                 noPaddingContent = { true }
174 223
                 onPopoverClose = { this._onHidePopover }
175 224
                 onPopoverOpen = { this._onShowPopover }
176
-                paddedContent = { true }
177 225
                 position = { statsPopoverPosition }
178 226
                 visible = { this.state.popoverVisible }>
179 227
                 { this._renderIndicator() }
@@ -231,13 +279,13 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
231 279
      * @returns {string}
232 280
      */
233 281
     _getVisibilityClass() {
234
-        const { _connectionStatus } = this.props;
282
+        const { _connectionStatus, classes } = this.props;
235 283
 
236 284
         return this.state.showIndicator
237 285
             || this.props.alwaysVisible
238 286
             || _connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED
239 287
             || _connectionStatus === JitsiParticipantConnectionStatus.INACTIVE
240
-            ? 'show-connection-indicator' : 'hide-connection-indicator';
288
+            ? '' : classes.hidden;
241 289
     }
242 290
 
243 291
     _onHidePopover: () => void;
@@ -259,6 +307,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
259 307
      * @returns {ReactElement}
260 308
      */
261 309
     _renderIcon() {
310
+        const colorClass = this._getConnectionColorClass();
311
+
262 312
         if (this.props._connectionStatus === JitsiParticipantConnectionStatus.INACTIVE) {
263 313
             if (this.props._connectionIndicatorInactiveDisabled) {
264 314
                 return null;
@@ -267,14 +317,13 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
267 317
             return (
268 318
                 <span className = 'connection_ninja'>
269 319
                     <Icon
270
-                        className = 'icon-ninja'
271
-                        size = '1.5em'
320
+                        className = { clsx(this.props.classes.icon, this.props.classes.inactiveIcon, colorClass) }
321
+                        size = { 24 }
272 322
                         src = { IconConnectionInactive } />
273 323
                 </span>
274 324
             );
275 325
         }
276 326
 
277
-        let iconWidth;
278 327
         let emptyIconWrapperClassName = 'connection_empty';
279 328
 
280 329
         if (this.props._connectionStatus
@@ -283,34 +332,16 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
283 332
             // emptyIconWrapperClassName is used by the torture tests to
284 333
             // identify lost connection status handling.
285 334
             emptyIconWrapperClassName = 'connection_lost';
286
-            iconWidth = '0%';
287
-        } else if (typeof this.state.stats.percent === 'undefined') {
288
-            iconWidth = '100%';
289
-        } else {
290
-            const { percent } = this.state.stats;
291
-
292
-            iconWidth = this._getDisplayConfiguration(percent).width;
293 335
         }
294 336
 
295
-        return [
296
-            <span
297
-                className = { emptyIconWrapperClassName }
298
-                key = 'icon-empty'>
299
-                <Icon
300
-                    className = 'icon-gsm-bars'
301
-                    size = '1em'
302
-                    src = { IconConnectionActive } />
303
-            </span>,
304
-            <span
305
-                className = 'connection_full'
306
-                key = 'icon-full'
307
-                style = {{ width: iconWidth }}>
337
+        return (
338
+            <span className = { emptyIconWrapperClassName }>
308 339
                 <Icon
309
-                    className = 'icon-gsm-bars'
310
-                    size = '1em'
340
+                    className = { clsx(this.props.classes.icon, colorClass) }
341
+                    size = { 12 }
311 342
                     src = { IconConnectionActive } />
312 343
             </span>
313
-        ];
344
+        );
314 345
     }
315 346
 
316 347
     _onShowPopover: () => void;
@@ -332,19 +363,10 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
332 363
      * @returns {ReactElement}
333 364
      */
334 365
     _renderIndicator() {
335
-        const colorClass = this._getConnectionColorClass();
336
-        const indicatorContainerClassNames
337
-              = `connection-indicator indicator ${colorClass}`;
338
-
339 366
         return (
340
-            <div className = 'popover-trigger'>
341
-                <div
342
-                    className = { indicatorContainerClassNames }
343
-                    style = {{ fontSize: this.props.iconSize }}>
344
-                    <div className = 'connection indicatoricon'>
345
-                        { this._renderIcon() }
346
-                    </div>
347
-                </div>
367
+            <div
368
+                style = {{ fontSize: this.props.iconSize }}>
369
+                {this._renderIcon()}
348 370
             </div>
349 371
         );
350 372
     }
@@ -369,4 +391,5 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
369 391
         _connectionStatus: participant?.connectionStatus
370 392
     };
371 393
 }
372
-export default translate(connect(_mapStateToProps)(ConnectionIndicator));
394
+export default translate(connect(_mapStateToProps)(
395
+    withStyles(styles)(ConnectionIndicator)));

+ 37
- 11
react/features/connection-stats/components/ConnectionStatsTable.js Bestand weergeven

@@ -1,8 +1,10 @@
1 1
 /* @flow */
2 2
 
3
+import { withStyles } from '@material-ui/styles';
3 4
 import React, { Component } from 'react';
4 5
 
5 6
 import { isMobileBrowser } from '../../../features/base/environment/utils';
7
+import ContextMenu from '../../base/components/context-menu/ContextMenu';
6 8
 import { translate } from '../../base/i18n';
7 9
 
8 10
 /**
@@ -40,6 +42,11 @@ type Props = {
40 42
      */
41 43
     bridgeCount: number,
42 44
 
45
+    /**
46
+     * An object containing the CSS classes.
47
+     */
48
+    classes: Object,
49
+
43 50
     /**
44 51
      * Audio/video codecs in use for the connection.
45 52
      */
@@ -163,6 +170,20 @@ function onClick(event) {
163 170
     event.stopPropagation();
164 171
 }
165 172
 
173
+const styles = theme => {
174
+    return {
175
+        contextMenu: {
176
+            position: 'relative',
177
+            marginTop: 0,
178
+            right: 'auto',
179
+            padding: `${theme.spacing(2)}px ${theme.spacing(1)}px`,
180
+            marginLeft: '4px',
181
+            marginRight: '4px',
182
+            marginBottom: '4px'
183
+        }
184
+    };
185
+};
186
+
166 187
 /**
167 188
  * React {@code Component} for displaying connection statistics.
168 189
  *
@@ -176,20 +197,25 @@ class ConnectionStatsTable extends Component<Props> {
176 197
      * @returns {ReactElement}
177 198
      */
178 199
     render() {
179
-        const { isLocalVideo, enableSaveLogs, disableShowMoreStats } = this.props;
200
+        const { isLocalVideo, enableSaveLogs, disableShowMoreStats, classes } = this.props;
180 201
         const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
181 202
 
182 203
         return (
183
-            <div
184
-                className = { className }
185
-                onClick = { onClick }>
186
-                { this._renderStatistics() }
187
-                <div className = 'connection-actions'>
188
-                    { isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
189
-                    { !disableShowMoreStats && this._renderShowMoreLink() }
204
+            <ContextMenu
205
+                className = { classes.contextMenu }
206
+                hidden = { false }
207
+                inDrawer = { true }>
208
+                <div
209
+                    className = { className }
210
+                    onClick = { onClick }>
211
+                    { this._renderStatistics() }
212
+                    <div className = 'connection-actions'>
213
+                        { isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
214
+                        { !disableShowMoreStats && this._renderShowMoreLink() }
215
+                    </div>
216
+                    { this.props.shouldShowMore ? this._renderAdditionalStats() : null }
190 217
                 </div>
191
-                { this.props.shouldShowMore ? this._renderAdditionalStats() : null }
192
-            </div>
218
+            </ContextMenu>
193 219
         );
194 220
     }
195 221
 
@@ -839,4 +865,4 @@ function getStringFromArray(array) {
839 865
     return res;
840 866
 }
841 867
 
842
-export default translate(ConnectionStatsTable);
868
+export default translate(withStyles(styles)(ConnectionStatsTable));

+ 66
- 9
react/features/display-name/components/web/DisplayName.js Bestand weergeven

@@ -1,5 +1,6 @@
1 1
 /* @flow */
2 2
 
3
+import { withStyles } from '@material-ui/styles';
3 4
 import React, { Component } from 'react';
4 5
 import type { Dispatch } from 'redux';
5 6
 
@@ -10,6 +11,8 @@ import {
10 11
 } from '../../../base/participants';
11 12
 import { connect } from '../../../base/redux';
12 13
 import { updateSettings } from '../../../base/settings';
14
+import { Tooltip } from '../../../base/tooltip';
15
+import { getIndicatorsTooltipPosition } from '../../../filmstrip/functions.web';
13 16
 import { appendSuffix } from '../../functions';
14 17
 
15 18
 /**
@@ -33,6 +36,11 @@ type Props = {
33 36
      */
34 37
     allowEditing: boolean,
35 38
 
39
+    /**
40
+     * The current layout of the filmstrip.
41
+     */
42
+    currentLayout: string,
43
+
36 44
     /**
37 45
      * Invoked to update the participant's display name.
38 46
      */
@@ -43,6 +51,11 @@ type Props = {
43 51
      */
44 52
     displayNameSuffix: string,
45 53
 
54
+    /**
55
+     * An object containing the CSS classes.
56
+     */
57
+    classes: Object,
58
+
46 59
     /**
47 60
      * The ID attribute to add to the component. Useful for global querying for
48 61
      * the component by legacy components and torture tests.
@@ -76,6 +89,30 @@ type State = {
76 89
     isEditing: boolean
77 90
 };
78 91
 
92
+const styles = theme => {
93
+    return {
94
+        displayName: {
95
+            ...theme.typography.labelBold,
96
+            lineHeight: `${theme.typography.labelBold.lineHeight}px`,
97
+            color: theme.palette.text01,
98
+            overflow: 'hidden',
99
+            textOverflow: 'ellipsis',
100
+            whiteSpace: 'nowrap'
101
+        },
102
+
103
+        editDisplayName: {
104
+            outline: 'none',
105
+            border: 'none',
106
+            background: 'none',
107
+            boxShadow: 'none',
108
+            padding: 0,
109
+            ...theme.typography.labelBold,
110
+            lineHeight: `${theme.typography.labelBold.lineHeight}px`,
111
+            color: theme.palette.text01
112
+        }
113
+    };
114
+};
115
+
79 116
 /**
80 117
  * React {@code Component} for displaying and editing a participant's name.
81 118
  *
@@ -146,7 +183,9 @@ class DisplayName extends Component<Props, State> {
146 183
         const {
147 184
             _nameToDisplay,
148 185
             allowEditing,
186
+            currentLayout,
149 187
             displayNameSuffix,
188
+            classes,
150 189
             elementID,
151 190
             t
152 191
         } = this.props;
@@ -155,10 +194,11 @@ class DisplayName extends Component<Props, State> {
155 194
             return (
156 195
                 <input
157 196
                     autoFocus = { true }
158
-                    className = 'editdisplayname'
197
+                    className = { classes.editDisplayName }
159 198
                     id = 'editDisplayName'
160 199
                     onBlur = { this._onSubmit }
161 200
                     onChange = { this._onChange }
201
+                    onClick = { this._onClick }
162 202
                     onKeyDown = { this._onKeyDown }
163 203
                     placeholder = { t('defaultNickname') }
164 204
                     ref = { this._setNameInputRef }
@@ -169,15 +209,30 @@ class DisplayName extends Component<Props, State> {
169 209
         }
170 210
 
171 211
         return (
172
-            <span
173
-                className = 'displayname'
174
-                id = { elementID }
175
-                onClick = { this._onStartEditing }>
176
-                { appendSuffix(_nameToDisplay, displayNameSuffix) }
177
-            </span>
212
+            <Tooltip
213
+                content = { appendSuffix(_nameToDisplay, displayNameSuffix) }
214
+                position = { getIndicatorsTooltipPosition(currentLayout) }>
215
+                <span
216
+                    className = { `displayname ${classes.displayName}` }
217
+                    id = { elementID }
218
+                    onClick = { this._onStartEditing }>
219
+                    { appendSuffix(_nameToDisplay, displayNameSuffix) }
220
+                </span>
221
+            </Tooltip>
178 222
         );
179 223
     }
180 224
 
225
+    /**
226
+     * Stop click event propagation.
227
+     *
228
+     * @param {MouseEvent} e - The click event.
229
+     * @private
230
+     * @returns {void}
231
+     */
232
+    _onClick(e) {
233
+        e.stopPropagation();
234
+    }
235
+
181 236
     _onChange: () => void;
182 237
 
183 238
     /**
@@ -215,11 +270,13 @@ class DisplayName extends Component<Props, State> {
215 270
      * Updates the component to display an editable input field and sets the
216 271
      * initial value to the current display name.
217 272
      *
273
+     * @param {MouseEvent} e - The click event.
218 274
      * @private
219 275
      * @returns {void}
220 276
      */
221
-    _onStartEditing() {
277
+    _onStartEditing(e) {
222 278
         if (this.props.allowEditing) {
279
+            e.stopPropagation();
223 280
             this.setState({
224 281
                 isEditing: true,
225 282
                 editDisplayNameValue: this.props._configuredDisplayName
@@ -292,4 +349,4 @@ function _mapStateToProps(state, ownProps) {
292 349
     };
293 350
 }
294 351
 
295
-export default translate(connect(_mapStateToProps)(DisplayName));
352
+export default translate(connect(_mapStateToProps)(withStyles(styles)(DisplayName)));

+ 12
- 22
react/features/filmstrip/components/web/AudioMutedIndicator.js Bestand weergeven

@@ -1,8 +1,8 @@
1 1
 /* @flow */
2 2
 
3
-import React, { Component } from 'react';
3
+import React from 'react';
4 4
 
5
-import { IconMicDisabled } from '../../../base/icons';
5
+import { IconMicrophoneEmptySlash } from '../../../base/icons';
6 6
 import { BaseIndicator } from '../../../base/react';
7 7
 
8 8
 /**
@@ -19,26 +19,16 @@ type Props = {
19 19
 /**
20 20
  * React {@code Component} for showing an audio muted icon with a tooltip.
21 21
  *
22
- * @augments Component
22
+ * @returns {Component}
23 23
  */
24
-class AudioMutedIndicator extends Component<Props> {
25
-    /**
26
-     * Implements React's {@link Component#render()}.
27
-     *
28
-     * @inheritdoc
29
-     * @returns {ReactElement}
30
-     */
31
-    render() {
32
-        return (
33
-            <BaseIndicator
34
-                className = 'audioMuted toolbar-icon'
35
-                icon = { IconMicDisabled }
36
-                iconId = 'mic-disabled'
37
-                iconSize = { 13 }
38
-                tooltipKey = 'videothumbnail.mute'
39
-                tooltipPosition = { this.props.tooltipPosition } />
40
-        );
41
-    }
42
-}
24
+const AudioMutedIndicator = ({ tooltipPosition }: Props) => (
25
+    <BaseIndicator
26
+        icon = { IconMicrophoneEmptySlash }
27
+        iconId = 'mic-disabled'
28
+        iconSize = { 15 }
29
+        id = 'audioMuted'
30
+        tooltipKey = 'videothumbnail.mute'
31
+        tooltipPosition = { tooltipPosition } />
32
+);
43 33
 
44 34
 export default AudioMutedIndicator;

+ 0
- 51
react/features/filmstrip/components/web/DominantSpeakerIndicator.js Bestand weergeven

@@ -1,51 +0,0 @@
1
-/* @flow */
2
-
3
-import React, { Component } from 'react';
4
-
5
-import { IconDominantSpeaker } from '../../../base/icons';
6
-import { BaseIndicator } from '../../../base/react';
7
-
8
-/**
9
- * The type of the React {@code Component} props of
10
- * {@link DominantSpeakerIndicator}.
11
- */
12
-type Props = {
13
-
14
-    /**
15
-     * The font-size for the icon.
16
-     */
17
-    iconSize: number,
18
-
19
-    /**
20
-     * From which side of the indicator the tooltip should appear from.
21
-     */
22
-    tooltipPosition: string
23
-};
24
-
25
-/**
26
- * Thumbnail badge showing that the participant is the dominant speaker in
27
- * the conference.
28
- *
29
- * @augments Component
30
- */
31
-class DominantSpeakerIndicator extends Component<Props> {
32
-    /**
33
-     * Implements React's {@link Component#render()}.
34
-     *
35
-     * @inheritdoc
36
-     */
37
-    render() {
38
-        return (
39
-            <BaseIndicator
40
-                className = 'indicator show-inline'
41
-                icon = { IconDominantSpeaker }
42
-                iconClassName = 'indicatoricon'
43
-                iconSize = { `${this.props.iconSize}px` }
44
-                id = 'dominantspeakerindicator'
45
-                tooltipKey = 'speaker'
46
-                tooltipPosition = { this.props.tooltipPosition } />
47
-        );
48
-    }
49
-}
50
-
51
-export default DominantSpeakerIndicator;

+ 10
- 23
react/features/filmstrip/components/web/ModeratorIndicator.js Bestand weergeven

@@ -1,8 +1,8 @@
1 1
 /* @flow */
2 2
 
3
-import React, { Component } from 'react';
3
+import React from 'react';
4 4
 
5
-import { IconModerator } from '../../../base/icons';
5
+import { IconCrown } from '../../../base/icons';
6 6
 import { BaseIndicator } from '../../../base/react';
7 7
 
8 8
 /**
@@ -19,27 +19,14 @@ type Props = {
19 19
 /**
20 20
  * React {@code Component} for showing a moderator icon with a tooltip.
21 21
  *
22
- * @augments Component
22
+ * @returns {Component}
23 23
  */
24
-class ModeratorIndicator extends Component<Props> {
25
-    /**
26
-     * Implements React's {@link Component#render()}.
27
-     *
28
-     * @inheritdoc
29
-     * @returns {ReactElement}
30
-     */
31
-    render() {
32
-        return (
33
-            <div className = 'moderator-icon right'>
34
-                <BaseIndicator
35
-                    className = 'focusindicator toolbar-icon'
36
-                    icon = { IconModerator }
37
-                    iconSize = { 13 }
38
-                    tooltipKey = 'videothumbnail.moderator'
39
-                    tooltipPosition = { this.props.tooltipPosition } />
40
-            </div>
41
-        );
42
-    }
43
-}
24
+const ModeratorIndicator = ({ tooltipPosition }: Props) => (
25
+    <BaseIndicator
26
+        icon = { IconCrown }
27
+        iconSize = { 15 }
28
+        tooltipKey = 'videothumbnail.moderator'
29
+        tooltipPosition = { tooltipPosition } />
30
+);
44 31
 
45 32
 export default ModeratorIndicator;

+ 45
- 23
react/features/filmstrip/components/web/RaisedHandIndicator.js Bestand weergeven

@@ -1,53 +1,75 @@
1 1
 /* @flow */
2 2
 
3
+import { makeStyles } from '@material-ui/styles';
3 4
 import React from 'react';
5
+import { useSelector } from 'react-redux';
4 6
 
5 7
 import { IconRaisedHand } from '../../../base/icons';
8
+import { getParticipantById, hasRaisedHand } from '../../../base/participants';
6 9
 import { BaseIndicator } from '../../../base/react';
7
-import { connect } from '../../../base/redux';
8
-import AbstractRaisedHandIndicator, {
9
-    type Props as AbstractProps,
10
-    _mapStateToProps
11
-} from '../AbstractRaisedHandIndicator';
12 10
 
13 11
 /**
14 12
  * The type of the React {@code Component} props of {@link RaisedHandIndicator}.
15 13
  */
16
-type Props = AbstractProps & {
14
+type Props = {
17 15
 
18 16
     /**
19 17
      * The font-size for the icon.
20 18
      */
21 19
     iconSize: number,
22 20
 
21
+    /**
22
+     * The participant id who we want to render the raised hand indicator
23
+     * for.
24
+     */
25
+    participantId: string,
26
+
23 27
     /**
24 28
      * From which side of the indicator the tooltip should appear from.
25 29
      */
26 30
     tooltipPosition: string
27 31
 };
28 32
 
33
+const useStyles = makeStyles(theme => {
34
+    return {
35
+        raisedHandIndicator: {
36
+            backgroundColor: theme.palette.warning01,
37
+            padding: '2px',
38
+            zIndex: 3,
39
+            display: 'inline-block',
40
+            borderRadius: '4px',
41
+            boxSizing: 'border-box'
42
+        }
43
+    };
44
+});
45
+
29 46
 /**
30 47
  * Thumbnail badge showing that the participant would like to speak.
31 48
  *
32
- * @augments Component
49
+ * @returns {ReactElement}
33 50
  */
34
-class RaisedHandIndicator extends AbstractRaisedHandIndicator<Props> {
35
-    /**
36
-     * Renders the platform specific indicator element.
37
-     *
38
-     * @returns {React$Element<*>}
39
-     */
40
-    _renderIndicator() {
41
-        return (
51
+const RaisedHandIndicator = ({
52
+    iconSize,
53
+    participantId,
54
+    tooltipPosition
55
+}: Props) => {
56
+    const _raisedHand = hasRaisedHand(useSelector(state =>
57
+        getParticipantById(state, participantId)));
58
+    const styles = useStyles();
59
+
60
+    if (!_raisedHand) {
61
+        return null;
62
+    }
63
+
64
+    return (
65
+        <div className = { styles.raisedHandIndicator }>
42 66
             <BaseIndicator
43
-                className = 'raisehandindicator indicator show-inline'
44 67
                 icon = { IconRaisedHand }
45
-                iconClassName = 'indicatoricon'
46
-                iconSize = { `${this.props.iconSize}px` }
68
+                iconSize = { `${iconSize}px` }
47 69
                 tooltipKey = 'raisedHand'
48
-                tooltipPosition = { this.props.tooltipPosition } />
49
-        );
50
-    }
51
-}
70
+                tooltipPosition = { tooltipPosition } />
71
+        </div>
72
+    );
73
+};
52 74
 
53
-export default connect(_mapStateToProps)(RaisedHandIndicator);
75
+export default RaisedHandIndicator;

+ 1
- 2
react/features/filmstrip/components/web/ScreenShareIndicator.js Bestand weergeven

@@ -23,10 +23,9 @@ type Props = {
23 23
 export default function ScreenShareIndicator(props: Props) {
24 24
     return (
25 25
         <BaseIndicator
26
-            className = 'screenShare toolbar-icon'
27 26
             icon = { IconShareDesktop }
28 27
             iconId = 'share-desktop'
29
-            iconSize = { 13 }
28
+            iconSize = { 15 }
30 29
             tooltipKey = 'videothumbnail.videomute'
31 30
             tooltipPosition = { props.tooltipPosition } />
32 31
     );

+ 13
- 35
react/features/filmstrip/components/web/StatusIndicators.js Bestand weergeven

@@ -6,12 +6,12 @@ import { MEDIA_TYPE } from '../../../base/media';
6 6
 import { getParticipantByIdOrUndefined, PARTICIPANT_ROLE } from '../../../base/participants';
7 7
 import { connect } from '../../../base/redux';
8 8
 import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
9
-import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
9
+import { getCurrentLayout } from '../../../video-layout';
10
+import { getIndicatorsTooltipPosition } from '../../functions.web';
10 11
 
11 12
 import AudioMutedIndicator from './AudioMutedIndicator';
12 13
 import ModeratorIndicator from './ModeratorIndicator';
13 14
 import ScreenShareIndicator from './ScreenShareIndicator';
14
-import VideoMutedIndicator from './VideoMutedIndicator';
15 15
 
16 16
 declare var interfaceConfig: Object;
17 17
 
@@ -40,11 +40,6 @@ type Props = {
40 40
      */
41 41
     _showScreenShareIndicator: Boolean,
42 42
 
43
-    /**
44
-     * Indicates if the video muted indicator should be visible or not.
45
-     */
46
-    _showVideoMutedIndicator: Boolean,
47
-
48 43
     /**
49 44
      * The ID of the participant for which the status bar is rendered.
50 45
      */
@@ -68,29 +63,16 @@ class StatusIndicators extends Component<Props> {
68 63
             _currentLayout,
69 64
             _showAudioMutedIndicator,
70 65
             _showModeratorIndicator,
71
-            _showScreenShareIndicator,
72
-            _showVideoMutedIndicator
66
+            _showScreenShareIndicator
73 67
         } = this.props;
74
-        let tooltipPosition;
75
-
76
-        switch (_currentLayout) {
77
-        case LAYOUTS.TILE_VIEW:
78
-            tooltipPosition = 'right';
79
-            break;
80
-        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
81
-            tooltipPosition = 'left';
82
-            break;
83
-        default:
84
-            tooltipPosition = 'top';
85
-        }
68
+        const tooltipPosition = getIndicatorsTooltipPosition(_currentLayout);
86 69
 
87 70
         return (
88
-            <div>
89
-                { _showAudioMutedIndicator ? <AudioMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
90
-                { _showScreenShareIndicator ? <ScreenShareIndicator tooltipPosition = { tooltipPosition } /> : null }
91
-                { _showVideoMutedIndicator ? <VideoMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
92
-                { _showModeratorIndicator ? <ModeratorIndicator tooltipPosition = { tooltipPosition } /> : null }
93
-            </div>
71
+            <>
72
+                { _showAudioMutedIndicator && <AudioMutedIndicator tooltipPosition = { tooltipPosition } /> }
73
+                { _showModeratorIndicator && <ModeratorIndicator tooltipPosition = { tooltipPosition } />}
74
+                { _showScreenShareIndicator && <ScreenShareIndicator tooltipPosition = { tooltipPosition } /> }
75
+            </>
94 76
         );
95 77
     }
96 78
 }
@@ -108,24 +90,21 @@ class StatusIndicators extends Component<Props> {
108 90
  * }}
109 91
 */
110 92
 function _mapStateToProps(state, ownProps) {
111
-    const { participantID } = ownProps;
93
+    const { participantID, audio, moderator, screenshare } = ownProps;
112 94
 
113 95
     // Only the local participant won't have id for the time when the conference is not yet joined.
114 96
     const participant = getParticipantByIdOrUndefined(state, participantID);
115 97
 
116 98
     const tracks = state['features/base/tracks'];
117
-    let isVideoMuted = true;
118 99
     let isAudioMuted = true;
119 100
     let isScreenSharing = false;
120 101
 
121 102
     if (participant?.local) {
122
-        isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
123 103
         isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
124 104
     } else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
125 105
         const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
126 106
 
127 107
         isScreenSharing = track?.videoType === 'desktop';
128
-        isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, participantID);
129 108
         isAudioMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID);
130 109
     }
131 110
 
@@ -133,11 +112,10 @@ function _mapStateToProps(state, ownProps) {
133 112
 
134 113
     return {
135 114
         _currentLayout: getCurrentLayout(state),
136
-        _showAudioMutedIndicator: isAudioMuted,
115
+        _showAudioMutedIndicator: isAudioMuted && audio,
137 116
         _showModeratorIndicator:
138
-            !disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR,
139
-        _showScreenShareIndicator: isScreenSharing,
140
-        _showVideoMutedIndicator: isVideoMuted
117
+            !disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR && moderator,
118
+        _showScreenShareIndicator: isScreenSharing && screenshare
141 119
     };
142 120
 }
143 121
 

+ 186
- 443
react/features/filmstrip/components/web/Thumbnail.js Bestand weergeven

@@ -1,17 +1,15 @@
1 1
 // @flow
2 2
 
3
+import { withStyles } from '@material-ui/styles';
4
+import clsx from 'clsx';
3 5
 import React, { Component } from 'react';
4 6
 
5 7
 import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
6
-import { AudioLevelIndicator } from '../../../audio-level-indicator';
7 8
 import { Avatar } from '../../../base/avatar';
8
-import { isNameReadOnly } from '../../../base/config';
9 9
 import { isMobileBrowser } from '../../../base/environment/utils';
10
-import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
11 10
 import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
12 11
 import {
13 12
     getParticipantByIdOrUndefined,
14
-    getParticipantCount,
15 13
     pinParticipant
16 14
 } from '../../../base/participants';
17 15
 import { connect } from '../../../base/redux';
@@ -23,23 +21,19 @@ import {
23 21
     getTrackByMediaTypeAndParticipant,
24 22
     updateLastTrackVideoMediaEvent
25 23
 } from '../../../base/tracks';
26
-import { ConnectionIndicator } from '../../../connection-indicator';
27
-import { DisplayName } from '../../../display-name';
28
-import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
29 24
 import { PresenceLabel } from '../../../presence-status';
30 25
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
31
-import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
32
-import { setVolume } from '../../actions.web';
33 26
 import {
34 27
     DISPLAY_MODE_TO_CLASS_NAME,
35 28
     DISPLAY_VIDEO,
36
-    DISPLAY_VIDEO_WITH_NAME,
37 29
     VIDEO_TEST_EVENTS,
38 30
     SHOW_TOOLBAR_CONTEXT_MENU_AFTER
39 31
 } from '../../constants';
40
-import { isVideoPlayable, computeDisplayMode } from '../../functions';
32
+import { isVideoPlayable, computeDisplayModeFromInput, getDisplayModeInput } from '../../functions';
41 33
 
42
-const JitsiTrackEvents = JitsiMeetJS.events.track;
34
+import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
35
+import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
36
+import ThumbnailTopIndicators from './ThumbnailTopIndicators';
43 37
 
44 38
 declare var interfaceConfig: Object;
45 39
 
@@ -48,11 +42,6 @@ declare var interfaceConfig: Object;
48 42
  */
49 43
 export type State = {|
50 44
 
51
-    /**
52
-     * The current audio level value for the Thumbnail.
53
-     */
54
-    audioLevel: number,
55
-
56 45
     /**
57 46
      * Indicates that the canplay event has been received.
58 47
      */
@@ -64,14 +53,14 @@ export type State = {|
64 53
     displayMode: number,
65 54
 
66 55
     /**
67
-     * Indicates whether the thumbnail is hovered or not.
56
+     * Whether popover is visible or not.
68 57
      */
69
-    isHovered: boolean,
58
+    popoverVisible: boolean,
70 59
 
71 60
     /**
72
-     * Whether popover is visible or not.
61
+     * Indicates whether the thumbnail is hovered or not.
73 62
      */
74
-    popoverVisible: boolean
63
+    isHovered: boolean
75 64
 |};
76 65
 
77 66
 /**
@@ -79,36 +68,16 @@ export type State = {|
79 68
  */
80 69
 export type Props = {|
81 70
 
82
-    /**
83
-     * If the display name is editable or not.
84
-     */
85
-    _allowEditing: boolean,
86
-
87 71
     /**
88 72
      * The audio track related to the participant.
89 73
      */
90 74
     _audioTrack: ?Object,
91 75
 
92
-    /**
93
-     * Disable/enable the auto hide functionality for the connection indicator.
94
-     */
95
-    _connectionIndicatorAutoHideEnabled: boolean,
96
-
97
-    /**
98
-     * Disable/enable the connection indicator.
99
-     */
100
-    _connectionIndicatorDisabled: boolean,
101
-
102 76
     /**
103 77
      * The current layout of the filmstrip.
104 78
      */
105 79
     _currentLayout: string,
106 80
 
107
-    /**
108
-     * The default display name for the local participant.
109
-     */
110
-    _defaultLocalDisplayName: string,
111
-
112 81
     /**
113 82
      * Indicates whether the local video flip feature is disabled or not.
114 83
      */
@@ -119,25 +88,20 @@ export type Props = {|
119 88
      */
120 89
     _disableTileEnlargement: boolean,
121 90
 
122
-    /**
123
-     * The display mode of the thumbnail.
124
-     */
125
-    _displayMode: number,
126
-
127 91
     /**
128 92
      * The height of the Thumbnail.
129 93
      */
130 94
     _height: number,
131 95
 
132 96
     /**
133
-     * The aspect ratio of the Thumbnail in percents.
97
+     * Indicates whether the thumbnail should be hidden or not.
134 98
      */
135
-    _heightToWidthPercent: number,
99
+    _isHidden: boolean,
136 100
 
137 101
     /**
138
-     * Indicates whether the thumbnail should be hidden or not.
102
+     * Whether or not there is a pinned participant.
139 103
      */
140
-    _isHidden: boolean,
104
+    _isAnyParticipantPinned: boolean,
141 105
 
142 106
     /**
143 107
      * Indicates whether audio only mode is enabled.
@@ -179,11 +143,6 @@ export type Props = {|
179 143
      */
180 144
     _isTestModeEnabled: boolean,
181 145
 
182
-    /**
183
-     * The size of the icon of indicators.
184
-     */
185
-    _indicatorIconSize: number,
186
-
187 146
     /**
188 147
      * The current local video flip setting.
189 148
      */
@@ -195,25 +154,10 @@ export type Props = {|
195 154
     _participant: Object,
196 155
 
197 156
     /**
198
-     * True if there are more than 2 participants in the call.
199
-     */
200
-     _participantCountMoreThan2: boolean,
201
-
202
-    /**
203
-     * Indicates whether the "start silent" mode is enabled.
204
-     */
205
-    _startSilent: Boolean,
206
-
207
-     /**
208 157
      * The video track that will be displayed in the thumbnail.
209 158
      */
210 159
     _videoTrack: ?Object,
211 160
 
212
-    /**
213
-     * The volume level for the thumbnail.
214
-     */
215
-    _volume?: ?number,
216
-
217 161
     /**
218 162
      * The width of the thumbnail.
219 163
      */
@@ -224,6 +168,11 @@ export type Props = {|
224 168
      */
225 169
     dispatch: Function,
226 170
 
171
+    /**
172
+     * An object containing the CSS classes.
173
+     */
174
+    classes: Object,
175
+
227 176
     /**
228 177
      * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
229 178
      */
@@ -240,17 +189,74 @@ export type Props = {|
240 189
     style?: ?Object
241 190
 |};
242 191
 
243
-/**
244
- * Click handler for the display name container.
245
- *
246
- * @param {SyntheticEvent} event - The click event.
247
- * @returns {void}
248
- */
249
-function onClick(event) {
250
-    // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
251
-    // needs to be stopped.
252
-    event.stopPropagation();
253
-}
192
+const defaultStyles = theme => {
193
+    return {
194
+        indicatorsContainer: {
195
+            position: 'absolute',
196
+            padding: `${theme.spacing(1)}px`,
197
+            zIndex: 10,
198
+            width: '100%',
199
+            boxSizing: 'border-box',
200
+            display: 'flex',
201
+            left: 0,
202
+
203
+            '&.tile-view-mode': {
204
+                padding: `${theme.spacing(2)}px`
205
+            }
206
+        },
207
+
208
+        indicatorsTopContainer: {
209
+            top: 0,
210
+            justifyContent: 'space-between'
211
+        },
212
+
213
+        indicatorsBottomContainer: {
214
+            bottom: 0
215
+        },
216
+
217
+        indicatorsBackground: {
218
+            backgroundColor: 'rgba(0, 0, 0, 0.7)',
219
+            borderRadius: '4px',
220
+            display: 'flex',
221
+            alignItems: 'center',
222
+            maxWidth: '100%',
223
+            overflow: 'hidden',
224
+
225
+            '&:not(:empty)': {
226
+                padding: '2px'
227
+            },
228
+
229
+            '& > *:not(:last-child)': {
230
+                marginRight: '4px'
231
+            },
232
+
233
+            '&:not(.top-indicators) > *:last-child': {
234
+                marginRight: '6px'
235
+            }
236
+        },
237
+
238
+        containerBackground: {
239
+            position: 'absolute',
240
+            top: 0,
241
+            left: 0,
242
+            height: '100%',
243
+            width: '100%',
244
+            borderRadius: '4px',
245
+            backgroundColor: theme.palette.ui02
246
+        },
247
+
248
+        activeSpeaker: {
249
+            '& .active-speaker-indicator': {
250
+                boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`,
251
+                position: 'absolute',
252
+                width: '100%',
253
+                height: '100%',
254
+                zIndex: '9',
255
+                borderRadius: '4px'
256
+            }
257
+        }
258
+    };
259
+};
254 260
 
255 261
 /**
256 262
  * Implements a thumbnail.
@@ -263,11 +269,6 @@ class Thumbnail extends Component<Props, State> {
263 269
      */
264 270
     timeoutHandle: Object;
265 271
 
266
-    /**
267
-     * Reference to local or remote Video Menu trigger button instance.
268
-     */
269
-    videoMenuTriggerRef: Object;
270
-
271 272
     /**
272 273
      * Timeout used to detect double tapping.
273 274
      * It is active while user has tapped once.
@@ -284,26 +285,21 @@ class Thumbnail extends Component<Props, State> {
284 285
         super(props);
285 286
 
286 287
         const state = {
287
-            audioLevel: 0,
288 288
             canPlayEventReceived: false,
289
-            isHovered: false,
290 289
             displayMode: DISPLAY_VIDEO,
291
-            popoverVisible: false
290
+            popoverVisible: false,
291
+            isHovered: false
292 292
         };
293 293
 
294 294
         this.state = {
295 295
             ...state,
296
-            displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state)),
297
-            popoverVisible: false
296
+            displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state))
298 297
         };
299 298
         this.timeoutHandle = null;
300
-        this.videoMenuTriggerRef = null;
301 299
 
302 300
         this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
303
-        this._updateAudioLevel = this._updateAudioLevel.bind(this);
304 301
         this._onCanPlay = this._onCanPlay.bind(this);
305 302
         this._onClick = this._onClick.bind(this);
306
-        this._onVolumeChange = this._onVolumeChange.bind(this);
307 303
         this._onMouseEnter = this._onMouseEnter.bind(this);
308 304
         this._onMouseLeave = this._onMouseLeave.bind(this);
309 305
         this._onTestingEvent = this._onTestingEvent.bind(this);
@@ -321,7 +317,6 @@ class Thumbnail extends Component<Props, State> {
321 317
      * @returns {void}
322 318
      */
323 319
     componentDidMount() {
324
-        this._listenForAudioUpdates();
325 320
         this._onDisplayModeChanged();
326 321
     }
327 322
 
@@ -333,12 +328,6 @@ class Thumbnail extends Component<Props, State> {
333 328
      * @returns {void}
334 329
      */
335 330
     componentDidUpdate(prevProps: Props, prevState: State) {
336
-        if (prevProps._audioTrack !== this.props._audioTrack) {
337
-            this._stopListeningForAudioUpdates(prevProps._audioTrack);
338
-            this._listenForAudioUpdates();
339
-            this._updateAudioLevel(0);
340
-        }
341
-
342 331
         if (prevState.displayMode !== this.state.displayMode) {
343 332
             this._onDisplayModeChanged();
344 333
         }
@@ -350,7 +339,7 @@ class Thumbnail extends Component<Props, State> {
350 339
      * @returns {void}
351 340
      */
352 341
     _onDisplayModeChanged() {
353
-        const input = Thumbnail.getDisplayModeInput(this.props, this.state);
342
+        const input = getDisplayModeInput(this.props, this.state);
354 343
 
355 344
         this._maybeSendScreenSharingIssueEvents(input);
356 345
     }
@@ -370,7 +359,7 @@ class Thumbnail extends Component<Props, State> {
370 359
         const { displayMode } = this.state;
371 360
         const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
372 361
 
373
-        if (![ DISPLAY_VIDEO, DISPLAY_VIDEO_WITH_NAME ].includes(displayMode)
362
+        if (!(DISPLAY_VIDEO === displayMode)
374 363
             && tileViewActive
375 364
             && _isScreenSharing
376 365
             && !_isAudioOnly) {
@@ -395,11 +384,11 @@ class Thumbnail extends Component<Props, State> {
395 384
 
396 385
             return {
397 386
                 ...newState,
398
-                displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, newState))
387
+                displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, newState))
399 388
             };
400 389
         }
401 390
 
402
-        const newDisplayMode = computeDisplayMode(Thumbnail.getDisplayModeInput(props, prevState));
391
+        const newDisplayMode = computeDisplayModeFromInput(getDisplayModeInput(props, prevState));
403 392
 
404 393
         if (newDisplayMode !== prevState.displayMode) {
405 394
             return {
@@ -411,51 +400,6 @@ class Thumbnail extends Component<Props, State> {
411 400
         return null;
412 401
     }
413 402
 
414
-    /**
415
-     * Extracts information for props and state needed to compute the display mode.
416
-     *
417
-     * @param {Props} props - The component's props.
418
-     * @param {State} state - The component's state.
419
-     * @returns {Object}
420
-     */
421
-    static getDisplayModeInput(props: Props, state: State) {
422
-        const {
423
-            _currentLayout,
424
-            _isAudioOnly,
425
-            _isCurrentlyOnLargeVideo,
426
-            _isScreenSharing,
427
-            _isVideoPlayable,
428
-            _participant,
429
-            _videoTrack
430
-        } = props;
431
-        const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
432
-        const { canPlayEventReceived, isHovered } = state;
433
-
434
-        return {
435
-            isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
436
-            isHovered,
437
-            isAudioOnly: _isAudioOnly,
438
-            tileViewActive,
439
-            isVideoPlayable: _isVideoPlayable,
440
-            connectionStatus: _participant?.connectionStatus,
441
-            canPlayEventReceived,
442
-            videoStream: Boolean(_videoTrack),
443
-            isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
444
-            isScreenSharing: _isScreenSharing,
445
-            videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
446
-        };
447
-    }
448
-
449
-    /**
450
-     * Unsubscribe from audio level updates.
451
-     *
452
-     * @inheritdoc
453
-     * @returns {void}
454
-     */
455
-    componentWillUnmount() {
456
-        this._stopListeningForAudioUpdates(this.props._audioTrack);
457
-    }
458
-
459 403
     _clearDoubleClickTimeout: () => void;
460 404
 
461 405
     /**
@@ -468,53 +412,6 @@ class Thumbnail extends Component<Props, State> {
468 412
         this._firstTap = undefined;
469 413
     }
470 414
 
471
-    /**
472
-     * Starts listening for audio level updates from the library.
473
-     *
474
-     * @private
475
-     * @returns {void}
476
-     */
477
-    _listenForAudioUpdates() {
478
-        const { _audioTrack } = this.props;
479
-
480
-        if (_audioTrack) {
481
-            const { jitsiTrack } = _audioTrack;
482
-
483
-            jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
484
-        }
485
-    }
486
-
487
-    /**
488
-     * Stops listening to further updates from the passed track.
489
-     *
490
-     * @param {Object} audioTrack - The track.
491
-     * @private
492
-     * @returns {void}
493
-     */
494
-    _stopListeningForAudioUpdates(audioTrack) {
495
-        if (audioTrack) {
496
-            const { jitsiTrack } = audioTrack;
497
-
498
-            jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
499
-        }
500
-    }
501
-
502
-    _updateAudioLevel: (number) => void;
503
-
504
-    /**
505
-     * Updates the internal state of the last know audio level. The level should
506
-     * be between 0 and 1, as the level will be used as a percentage out of 1.
507
-     *
508
-     * @param {number} audioLevel - The new audio level for the track.
509
-     * @private
510
-     * @returns {void}
511
-     */
512
-    _updateAudioLevel(audioLevel) {
513
-        this.setState({
514
-            audioLevel
515
-        });
516
-    }
517
-
518 415
     _showPopover: () => void;
519 416
 
520 417
     /**
@@ -549,7 +446,6 @@ class Thumbnail extends Component<Props, State> {
549 446
      * @returns {Object} - The styles for the thumbnail.
550 447
      */
551 448
     _getStyles(): Object {
552
-
553 449
         const { canPlayEventReceived } = this.state;
554 450
         const {
555 451
             _currentLayout,
@@ -575,7 +471,7 @@ class Thumbnail extends Component<Props, State> {
575 471
             video: {}
576 472
         };
577 473
 
578
-        const avatarSize = _height / 2;
474
+        const avatarSize = Math.min(_height / 2, _width - 30);
579 475
         let { left } = style || {};
580 476
 
581 477
         if (typeof left === 'number' && horizontalOffset) {
@@ -730,67 +626,6 @@ class Thumbnail extends Component<Props, State> {
730 626
         );
731 627
     }
732 628
 
733
-    /**
734
-     * Renders the top indicators of the thumbnail.
735
-     *
736
-     * @returns {Component}
737
-     */
738
-    _renderTopIndicators() {
739
-        const {
740
-            _connectionIndicatorAutoHideEnabled,
741
-            _connectionIndicatorDisabled,
742
-            _currentLayout,
743
-            _isDominantSpeakerDisabled,
744
-            _indicatorIconSize: iconSize,
745
-            _participant,
746
-            _participantCountMoreThan2
747
-        } = this.props;
748
-        const { isHovered } = this.state;
749
-        const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
750
-        const { id, dominantSpeaker = false } = _participant;
751
-        const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
752
-        let statsPopoverPosition, tooltipPosition;
753
-
754
-        switch (_currentLayout) {
755
-        case LAYOUTS.TILE_VIEW:
756
-            statsPopoverPosition = 'right-start';
757
-            tooltipPosition = 'right';
758
-            break;
759
-        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
760
-            statsPopoverPosition = 'left-start';
761
-            tooltipPosition = 'left';
762
-            break;
763
-        case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
764
-            statsPopoverPosition = 'top';
765
-            tooltipPosition = 'top';
766
-            break;
767
-        default:
768
-            statsPopoverPosition = 'auto';
769
-            tooltipPosition = 'top';
770
-        }
771
-
772
-        return (
773
-            <div>
774
-                { !_connectionIndicatorDisabled
775
-                    && <ConnectionIndicator
776
-                        alwaysVisible = { showConnectionIndicator }
777
-                        enableStatsDisplay = { true }
778
-                        iconSize = { iconSize }
779
-                        participantId = { id }
780
-                        statsPopoverPosition = { statsPopoverPosition } />
781
-                }
782
-                <RaisedHandIndicator
783
-                    iconSize = { iconSize }
784
-                    participantId = { id }
785
-                    tooltipPosition = { tooltipPosition } />
786
-                { showDominantSpeaker && _participantCountMoreThan2
787
-                    && <DominantSpeakerIndicator
788
-                        iconSize = { iconSize }
789
-                        tooltipPosition = { tooltipPosition } />
790
-                }
791
-            </div>);
792
-    }
793
-
794 629
     /**
795 630
      * Renders the avatar.
796 631
      *
@@ -820,113 +655,29 @@ class Thumbnail extends Component<Props, State> {
820 655
     _getContainerClassName() {
821 656
         let className = 'videocontainer';
822 657
         const { displayMode } = this.state;
823
-        const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props;
824
-        const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant;
825
-
826
-        className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
827
-
828
-        if (_participant?.pinned) {
829
-            className += ' videoContainerFocused';
830
-        }
831
-
832
-        if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
833
-            className += ' active-speaker';
834
-        }
835
-
836
-        if (_isHidden) {
837
-            className += ' hidden';
838
-        }
839
-
840
-        if (isRemoteParticipant && _isAudioOnly) {
841
-            className += ' audio-only';
842
-        }
843
-
844
-        return className;
845
-    }
846
-
847
-    /**
848
-     * Renders the local participant's thumbnail.
849
-     *
850
-     * @returns {ReactElement}
851
-     */
852
-    _renderLocalParticipant() {
853 658
         const {
854
-            _allowEditing,
855
-            _defaultLocalDisplayName,
856
-            _disableLocalVideoFlip,
857
-            _isMobile,
858
-            _isMobilePortrait,
859
-            _isScreenSharing,
860
-            _localFlipX,
659
+            _isDominantSpeakerDisabled,
861 660
             _participant,
862
-            _videoTrack
661
+            _currentLayout,
662
+            _isAnyParticipantPinned,
663
+            classes
863 664
         } = this.props;
864
-        const { id } = _participant || {};
865
-        const { audioLevel } = this.state;
866
-        const styles = this._getStyles();
867
-        let containerClassName = this._getContainerClassName();
868
-        const videoTrackClassName
869
-            = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
870 665
 
871
-        if (_isMobilePortrait) {
872
-            styles.thumbnail.height = styles.thumbnail.width;
873
-            containerClassName = `${containerClassName} self-view-mobile-portrait`;
874
-        }
666
+        className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
875 667
 
876
-        return (
877
-            <span
878
-                className = { containerClassName }
879
-                id = 'localVideoContainer'
880
-                { ...(_isMobile
881
-                    ? {
882
-                        onTouchEnd: this._onTouchEnd,
883
-                        onTouchMove: this._onTouchMove,
884
-                        onTouchStart: this._onTouchStart
885
-                    }
886
-                    : {
887
-                        onClick: this._onClick,
888
-                        onMouseEnter: this._onMouseEnter,
889
-                        onMouseLeave: this._onMouseLeave
890
-                    }
891
-                ) }
892
-                style = { styles.thumbnail }>
893
-                <div className = 'videocontainer__background' />
894
-                <span id = 'localVideoWrapper'>
895
-                    <VideoTrack
896
-                        className = { videoTrackClassName }
897
-                        id = 'localVideo_container'
898
-                        style = { styles.video }
899
-                        videoTrack = { _videoTrack } />
900
-                </span>
901
-                <div className = 'videocontainer__toolbar'>
902
-                    <StatusIndicators participantID = { id } />
903
-                    <div
904
-                        className = 'videocontainer__participant-name'
905
-                        onClick = { onClick }>
906
-                        <DisplayName
907
-                            allowEditing = { _allowEditing }
908
-                            displayNameSuffix = { _defaultLocalDisplayName }
909
-                            elementID = 'localDisplayName'
910
-                            participantID = { id } />
911
-                    </div>
912
-                </div>
913
-                <div className = 'videocontainer__toptoolbar'>
914
-                    { this._renderTopIndicators() }
915
-                </div>
916
-                <div className = 'videocontainer__hoverOverlay' />
917
-                { this._renderAvatar(styles.avatar) }
918
-                <span className = 'audioindicator-container'>
919
-                    <AudioLevelIndicator audioLevel = { audioLevel } />
920
-                </span>
921
-                <span className = 'localvideomenu'>
922
-                    <LocalVideoMenuTriggerButton
923
-                        hidePopover = { this._hidePopover }
924
-                        popoverVisible = { this.state.popoverVisible }
925
-                        showPopover = { this._showPopover } />
926
-                </span>
668
+        if (_currentLayout === LAYOUTS.TILE_VIEW) {
669
+            if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
670
+                className += ` ${classes.activeSpeaker} dominant-speaker`;
671
+            }
672
+        } else if (_isAnyParticipantPinned) {
673
+            if (_participant?.pinned) {
674
+                className += ` videoContainerFocused ${classes.activeSpeaker}`;
675
+            }
676
+        } else if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
677
+            className += ` ${classes.activeSpeaker} dominant-speaker`;
678
+        }
927 679
 
928
-            </span>
929
-        );
680
+        return className;
930 681
     }
931 682
 
932 683
     _onCanPlay: Object => void;
@@ -971,40 +722,59 @@ class Thumbnail extends Component<Props, State> {
971 722
     /**
972 723
      * Renders a remote participant's 'thumbnail.
973 724
      *
725
+     * @param {boolean} local - Whether or not it's the local participant.
974 726
      * @returns {ReactElement}
975 727
      */
976
-    _renderRemoteParticipant() {
728
+    _renderParticipant(local = false) {
977 729
         const {
730
+            _audioTrack,
731
+            _currentLayout,
732
+            _disableLocalVideoFlip,
978 733
             _isMobile,
734
+            _isMobilePortrait,
735
+            _isScreenSharing,
979 736
             _isTestModeEnabled,
737
+            _localFlipX,
980 738
             _participant,
981
-            _startSilent,
982 739
             _videoTrack,
983
-            _volume = 1
740
+            classes
984 741
         } = this.props;
985
-        const { id } = _participant;
986
-        const { audioLevel } = this.state;
742
+        const { id } = _participant || {};
743
+        const { isHovered, popoverVisible } = this.state;
987 744
         const styles = this._getStyles();
988
-        const containerClassName = this._getContainerClassName();
989
-
990
-        // hide volume when in silent mode
991
-        const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
745
+        let containerClassName = this._getContainerClassName();
746
+        const videoTrackClassName
747
+            = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
992 748
         const jitsiVideoTrack = _videoTrack?.jitsiTrack;
993 749
         const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
994 750
         const videoEventListeners = {};
995 751
 
996
-        if (_videoTrack && _isTestModeEnabled) {
997
-            VIDEO_TEST_EVENTS.forEach(attribute => {
998
-                videoEventListeners[attribute] = this._onTestingEvent;
999
-            });
752
+        if (local) {
753
+            if (_isMobilePortrait) {
754
+                styles.thumbnail.height = styles.thumbnail.width;
755
+                containerClassName = `${containerClassName} self-view-mobile-portrait`;
756
+            }
757
+        } else {
758
+            if (_videoTrack && _isTestModeEnabled) {
759
+                VIDEO_TEST_EVENTS.forEach(attribute => {
760
+                    videoEventListeners[attribute] = this._onTestingEvent;
761
+                });
762
+            }
763
+            videoEventListeners.onCanPlay = this._onCanPlay;
1000 764
         }
1001 765
 
1002
-        videoEventListeners.onCanPlay = this._onCanPlay;
766
+        const video = _videoTrack && <VideoTrack
767
+            className = { local ? videoTrackClassName : '' }
768
+            eventHandlers = { videoEventListeners }
769
+            id = { local ? 'localVideo_container' : `remoteVideo_${videoTrackId || ''}` }
770
+            muted = { local ? undefined : true }
771
+            style = { styles.video }
772
+            videoTrack = { _videoTrack } />;
1003 773
 
1004 774
         return (
1005 775
             <span
1006 776
                 className = { containerClassName }
1007
-                id = { `participant_${id}` }
777
+                id = { local ? 'localVideoContainer' : `participant_${id}` }
1008 778
                 { ...(_isMobile
1009 779
                     ? {
1010 780
                         onTouchEnd: this._onTouchEnd,
@@ -1018,64 +788,50 @@ class Thumbnail extends Component<Props, State> {
1018 788
                     }
1019 789
                 ) }
1020 790
                 style = { styles.thumbnail }>
1021
-                {
1022
-                    _videoTrack && <VideoTrack
1023
-                        eventHandlers = { videoEventListeners }
1024
-                        id = { `remoteVideo_${videoTrackId || ''}` }
1025
-                        muted = { true }
1026
-                        style = { styles.video }
1027
-                        videoTrack = { _videoTrack } />
1028
-                }
1029
-                <div className = 'videocontainer__background' />
1030
-                <div className = 'videocontainer__toptoolbar'>
1031
-                    { this._renderTopIndicators() }
791
+                {local
792
+                    ? <span id = 'localVideoWrapper'>{video}</span>
793
+                    : video}
794
+                <div className = { classes.containerBackground } />
795
+                <div
796
+                    className = { clsx(classes.indicatorsContainer,
797
+                        classes.indicatorsTopContainer,
798
+                        _currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
799
+                    ) }>
800
+                    <ThumbnailTopIndicators
801
+                        currentLayout = { _currentLayout }
802
+                        hidePopover = { this._hidePopover }
803
+                        indicatorsClassName = { classes.indicatorsBackground }
804
+                        isHovered = { isHovered }
805
+                        local = { local }
806
+                        participantId = { id }
807
+                        popoverVisible = { popoverVisible }
808
+                        showPopover = { this._showPopover } />
1032 809
                 </div>
1033
-                <div className = 'videocontainer__toolbar'>
1034
-                    <StatusIndicators participantID = { id } />
1035
-                    <div className = 'videocontainer__participant-name'>
1036
-                        <DisplayName
1037
-                            elementID = { `participant_${id}_name` }
1038
-                            participantID = { id } />
1039
-                    </div>
810
+                <div
811
+                    className = { clsx(classes.indicatorsContainer,
812
+                        classes.indicatorsBottomContainer,
813
+                        _currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
814
+                    ) }>
815
+                    <ThumbnailBottomIndicators
816
+                        className = { classes.indicatorsBackground }
817
+                        currentLayout = { _currentLayout }
818
+                        local = { local }
819
+                        participantId = { id } />
1040 820
                 </div>
1041
-                <div className = 'videocontainer__hoverOverlay' />
1042 821
                 { this._renderAvatar(styles.avatar) }
1043
-                <div className = 'presence-label-container'>
1044
-                    <PresenceLabel
1045
-                        className = 'presence-label'
1046
-                        participantID = { id } />
1047
-                </div>
1048
-                <span className = 'audioindicator-container'>
1049
-                    <AudioLevelIndicator audioLevel = { audioLevel } />
1050
-                </span>
1051
-                <span className = 'remotevideomenu'>
1052
-                    <RemoteVideoMenuTriggerButton
1053
-                        hidePopover = { this._hidePopover }
1054
-                        initialVolumeValue = { _volume }
1055
-                        onVolumeChange = { onVolumeChange }
1056
-                        participantID = { id }
1057
-                        popoverVisible = { this.state.popoverVisible }
1058
-                        showPopover = { this._showPopover } />
1059
-                </span>
822
+                { !local && (
823
+                    <div className = 'presence-label-container'>
824
+                        <PresenceLabel
825
+                            className = 'presence-label'
826
+                            participantID = { id } />
827
+                    </div>
828
+                )}
829
+                <ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
830
+                <div className = 'active-speaker-indicator' />
1060 831
             </span>
1061 832
         );
1062 833
     }
1063 834
 
1064
-    _onVolumeChange: number => void;
1065
-
1066
-    /**
1067
-     * Handles volume changes.
1068
-     *
1069
-     * @param {number} value - The new value for the volume.
1070
-     * @returns {void}
1071
-     */
1072
-    _onVolumeChange(value) {
1073
-        const { _participant, dispatch } = this.props;
1074
-        const { id } = _participant;
1075
-
1076
-        dispatch(setVolume(id, value));
1077
-    }
1078
-
1079 835
     /**
1080 836
      * Implements React's {@link Component#render()}.
1081 837
      *
@@ -1092,14 +848,14 @@ class Thumbnail extends Component<Props, State> {
1092 848
         const { isFakeParticipant, local } = _participant;
1093 849
 
1094 850
         if (local) {
1095
-            return this._renderLocalParticipant();
851
+            return this._renderParticipant(true);
1096 852
         }
1097 853
 
1098 854
         if (isFakeParticipant) {
1099 855
             return this._renderFakeParticipant();
1100 856
         }
1101 857
 
1102
-        return this._renderRemoteParticipant();
858
+        return this._renderParticipant();
1103 859
     }
1104 860
 }
1105 861
 
@@ -1118,7 +874,6 @@ function _mapStateToProps(state, ownProps): Object {
1118 874
     const id = participant?.id;
1119 875
     const isLocal = participant?.local ?? true;
1120 876
     const tracks = state['features/base/tracks'];
1121
-    const { participantsVolume } = state['features/filmstrip'];
1122 877
     const _videoTrack = isLocal
1123 878
         ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
1124 879
     const _audioTrack = isLocal
@@ -1127,14 +882,12 @@ function _mapStateToProps(state, ownProps): Object {
1127 882
     let size = {};
1128 883
     let _isMobilePortrait = false;
1129 884
     const {
1130
-        startSilent,
1131 885
         defaultLocalDisplayName,
1132 886
         disableLocalVideoFlip,
1133 887
         disableTileEnlargement,
1134 888
         iAmRecorder,
1135 889
         iAmSipGateway
1136 890
     } = state['features/base/config'];
1137
-    const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
1138 891
     const { localFlipX } = state['features/base/settings'];
1139 892
     const _isMobile = isMobileBrowser();
1140 893
 
@@ -1167,7 +920,6 @@ function _mapStateToProps(state, ownProps): Object {
1167 920
         break;
1168 921
     }
1169 922
     case LAYOUTS.TILE_VIEW: {
1170
-
1171 923
         const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
1172 924
 
1173 925
         size = {
@@ -1179,12 +931,7 @@ function _mapStateToProps(state, ownProps): Object {
1179 931
     }
1180 932
 
1181 933
     return {
1182
-        _allowEditing: !isNameReadOnly(state),
1183 934
         _audioTrack,
1184
-        _connectionIndicatorAutoHideEnabled:
1185
-        Boolean(state['features/base/config'].connectionIndicators?.autoHide ?? true),
1186
-        _connectionIndicatorDisabled: _isMobile
1187
-            || Boolean(state['features/base/config'].connectionIndicators?.disabled),
1188 935
         _currentLayout,
1189 936
         _defaultLocalDisplayName: defaultLocalDisplayName,
1190 937
         _disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
@@ -1198,15 +945,11 @@ function _mapStateToProps(state, ownProps): Object {
1198 945
         _isScreenSharing: _videoTrack?.videoType === 'desktop',
1199 946
         _isTestModeEnabled: isTestModeEnabled(state),
1200 947
         _isVideoPlayable: id && isVideoPlayable(state, id),
1201
-        _indicatorIconSize: NORMAL,
1202 948
         _localFlipX: Boolean(localFlipX),
1203 949
         _participant: participant,
1204
-        _participantCountMoreThan2: getParticipantCount(state) > 2,
1205
-        _startSilent: Boolean(startSilent),
1206 950
         _videoTrack,
1207
-        _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined,
1208 951
         ...size
1209 952
     };
1210 953
 }
1211 954
 
1212
-export default connect(_mapStateToProps)(Thumbnail);
955
+export default connect(_mapStateToProps)(withStyles(defaultStyles)(Thumbnail));

+ 47
- 0
react/features/filmstrip/components/web/ThumbnailAudioIndicator.js Bestand weergeven

@@ -0,0 +1,47 @@
1
+// @flow
2
+
3
+import React, { useEffect, useState } from 'react';
4
+
5
+import { AudioLevelIndicator } from '../../../audio-level-indicator';
6
+import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
7
+
8
+const JitsiTrackEvents = JitsiMeetJS.events.track;
9
+
10
+type Props = {
11
+
12
+    /**
13
+     * The audio track related to the participant.
14
+     */
15
+    _audioTrack: ?Object
16
+}
17
+
18
+const ThumbnailAudioIndicator = ({
19
+    _audioTrack
20
+}: Props) => {
21
+    const [ audioLevel, setAudioLevel ] = useState(0);
22
+
23
+    useEffect(() => {
24
+        setAudioLevel(0);
25
+        if (_audioTrack) {
26
+            const { jitsiTrack } = _audioTrack;
27
+
28
+            jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, setAudioLevel);
29
+        }
30
+
31
+        return () => {
32
+            if (_audioTrack) {
33
+                const { jitsiTrack } = _audioTrack;
34
+
35
+                jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, setAudioLevel);
36
+            }
37
+        };
38
+    }, [ _audioTrack ]);
39
+
40
+    return (
41
+        <span className = 'audioindicator-container'>
42
+            <AudioLevelIndicator audioLevel = { audioLevel } />
43
+        </span>
44
+    );
45
+};
46
+
47
+export default ThumbnailAudioIndicator;

+ 84
- 0
react/features/filmstrip/components/web/ThumbnailBottomIndicators.js Bestand weergeven

@@ -0,0 +1,84 @@
1
+// @flow
2
+
3
+import { makeStyles } from '@material-ui/styles';
4
+import React from 'react';
5
+import { useSelector } from 'react-redux';
6
+
7
+import { isNameReadOnly } from '../../../base/config/functions.any';
8
+import DisplayName from '../../../display-name/components/web/DisplayName';
9
+import { LAYOUTS } from '../../../video-layout';
10
+
11
+import StatusIndicators from './StatusIndicators';
12
+
13
+declare var interfaceConfig: Object;
14
+
15
+type Props = {
16
+
17
+    /**
18
+     * The current layout of the filmstrip.
19
+     */
20
+    currentLayout: string,
21
+
22
+    /**
23
+     * Class name for indicators container.
24
+     */
25
+    className: string,
26
+
27
+    /**
28
+     * Whether or not the indicators are for the local participant.
29
+     */
30
+    local: boolean,
31
+
32
+    /**
33
+     * Id of the participant for which the component is displayed.
34
+     */
35
+    participantId: string
36
+}
37
+
38
+const useStyles = makeStyles(() => {
39
+    return {
40
+        nameContainer: {
41
+            display: 'flex',
42
+            overflow: 'hidden',
43
+            padding: '2px 0',
44
+
45
+            '&>div': {
46
+                display: 'flex',
47
+                overflow: 'hidden'
48
+            },
49
+
50
+            '&:first-child': {
51
+                marginLeft: '6px'
52
+            }
53
+        }
54
+    };
55
+});
56
+
57
+const ThumbnailBottomIndicators = ({
58
+    className,
59
+    currentLayout,
60
+    local,
61
+    participantId
62
+}: Props) => {
63
+    const styles = useStyles();
64
+    const _allowEditing = !useSelector(isNameReadOnly);
65
+    const _defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
66
+
67
+    return (<div className = { className }>
68
+        <StatusIndicators
69
+            audio = { true }
70
+            moderator = { true }
71
+            participantID = { participantId }
72
+            screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
73
+        <span className = { styles.nameContainer }>
74
+            <DisplayName
75
+                allowEditing = { local ? _allowEditing : false }
76
+                currentLayout = { currentLayout }
77
+                displayNameSuffix = { local ? _defaultLocalDisplayName : '' }
78
+                elementID = { local ? 'localDisplayName' : `participant_${participantId}_name` }
79
+                participantID = { participantId } />
80
+        </span>
81
+    </div>);
82
+};
83
+
84
+export default ThumbnailBottomIndicators;

+ 132
- 0
react/features/filmstrip/components/web/ThumbnailTopIndicators.js Bestand weergeven

@@ -0,0 +1,132 @@
1
+// @flow
2
+
3
+import { makeStyles } from '@material-ui/styles';
4
+import clsx from 'clsx';
5
+import React from 'react';
6
+import { useSelector } from 'react-redux';
7
+
8
+import { isMobileBrowser } from '../../../base/environment/utils';
9
+import ConnectionIndicator from '../../../connection-indicator/components/web/ConnectionIndicator';
10
+import { LAYOUTS } from '../../../video-layout';
11
+import { STATS_POPOVER_POSITION } from '../../constants';
12
+import { getIndicatorsTooltipPosition } from '../../functions.web';
13
+
14
+import RaisedHandIndicator from './RaisedHandIndicator';
15
+import StatusIndicators from './StatusIndicators';
16
+import VideoMenuTriggerButton from './VideoMenuTriggerButton';
17
+
18
+declare var interfaceConfig: Object;
19
+
20
+type Props = {
21
+
22
+    /**
23
+     * The current layout of the filmstrip.
24
+     */
25
+    currentLayout: string,
26
+
27
+    /**
28
+     * Hide popover callback.
29
+     */
30
+    hidePopover: Function,
31
+
32
+    /**
33
+     * Class name for the status indicators container.
34
+     */
35
+    indicatorsClassName: string,
36
+
37
+    /**
38
+     * Whether or not the thumbnail is hovered.
39
+     */
40
+    isHovered: boolean,
41
+
42
+    /**
43
+     * Whether or not the indicators are for the local participant.
44
+     */
45
+    local: boolean,
46
+
47
+    /**
48
+     * Id of the participant for which the component is displayed.
49
+     */
50
+    participantId: string,
51
+
52
+    /**
53
+     * Whether popover is visible or not.
54
+     */
55
+    popoverVisible: boolean,
56
+
57
+    /**
58
+     * Show popover callback.
59
+     */
60
+    showPopover: Function
61
+}
62
+
63
+const useStyles = makeStyles(() => {
64
+    return {
65
+        container: {
66
+            display: 'flex',
67
+
68
+            '& > *:not(:last-child)': {
69
+                marginRight: '4px'
70
+            }
71
+        }
72
+    };
73
+});
74
+
75
+const ThumbnailTopIndicators = ({
76
+    currentLayout,
77
+    hidePopover,
78
+    indicatorsClassName,
79
+    isHovered,
80
+    local,
81
+    participantId,
82
+    popoverVisible,
83
+    showPopover
84
+}: Props) => {
85
+    const styles = useStyles();
86
+
87
+    const _isMobile = isMobileBrowser();
88
+    const { NORMAL = 16 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
89
+    const _indicatorIconSize = NORMAL;
90
+    const _connectionIndicatorAutoHideEnabled = Boolean(
91
+        useSelector(state => state['features/base/config'].connectionIndicators?.autoHide) ?? true);
92
+    const _connectionIndicatorDisabled = _isMobile
93
+        || Boolean(useSelector(state => state['features/base/config'].connectionIndicators?.disabled));
94
+
95
+    const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
96
+
97
+    return (
98
+        <>
99
+            <div className = { styles.container }>
100
+                {!_connectionIndicatorDisabled
101
+                    && <ConnectionIndicator
102
+                        alwaysVisible = { showConnectionIndicator }
103
+                        enableStatsDisplay = { true }
104
+                        iconSize = { _indicatorIconSize }
105
+                        participantId = { participantId }
106
+                        statsPopoverPosition = { STATS_POPOVER_POSITION[currentLayout] } />
107
+                }
108
+                <RaisedHandIndicator
109
+                    iconSize = { _indicatorIconSize }
110
+                    participantId = { participantId }
111
+                    tooltipPosition = { getIndicatorsTooltipPosition(currentLayout) } />
112
+                {currentLayout === LAYOUTS.TILE_VIEW && (
113
+                    <div className = { clsx(indicatorsClassName, 'top-indicators') }>
114
+                        <StatusIndicators
115
+                            participantID = { participantId }
116
+                            screenshare = { true } />
117
+                    </div>
118
+                )}
119
+            </div>
120
+            <div className = { styles.container }>
121
+                <VideoMenuTriggerButton
122
+                    hidePopover = { hidePopover }
123
+                    local = { local }
124
+                    participantId = { participantId }
125
+                    popoverVisible = { popoverVisible }
126
+                    showPopover = { showPopover }
127
+                    visible = { isHovered } />
128
+            </div>
129
+        </>);
130
+};
131
+
132
+export default ThumbnailTopIndicators;

+ 12
- 2
react/features/filmstrip/components/web/ThumbnailWrapper.js Bestand weergeven

@@ -23,6 +23,11 @@ type Props = {
23 23
      */
24 24
     _horizontalOffset: number,
25 25
 
26
+    /**
27
+     * Whether or not there is a pinned participant.
28
+     */
29
+    _isAnyParticipantPinned: boolean,
30
+
26 31
     /**
27 32
      * The ID of the participant associated with the Thumbnail.
28 33
      */
@@ -75,7 +80,7 @@ class ThumbnailWrapper extends Component<Props> {
75 80
      * @returns {ReactElement}
76 81
      */
77 82
     render() {
78
-        const { _participantID, style, _horizontalOffset = 0, _disableSelfView } = this.props;
83
+        const { _participantID, style, _horizontalOffset = 0, _isAnyParticipantPinned, _disableSelfView } = this.props;
79 84
 
80 85
         if (typeof _participantID !== 'string') {
81 86
             return null;
@@ -91,6 +96,7 @@ class ThumbnailWrapper extends Component<Props> {
91 96
 
92 97
         return (
93 98
             <Thumbnail
99
+                _isAnyParticipantPinned = { _isAnyParticipantPinned }
94 100
                 horizontalOffset = { _horizontalOffset }
95 101
                 key = { `remote_${_participantID}` }
96 102
                 participantID = { _participantID }
@@ -109,6 +115,7 @@ class ThumbnailWrapper extends Component<Props> {
109 115
 function _mapStateToProps(state, ownProps) {
110 116
     const _currentLayout = getCurrentLayout(state);
111 117
     const { remoteParticipants } = state['features/filmstrip'];
118
+    const { remote, local } = state['features/base/participants'];
112 119
     const remoteParticipantsLength = remoteParticipants.length;
113 120
     const { testing = {} } = state['features/base/config'];
114 121
     const disableSelfView = getDisableSelfView(state);
@@ -160,8 +167,11 @@ function _mapStateToProps(state, ownProps) {
160 167
         return {};
161 168
     }
162 169
 
170
+    const _isAnyParticipantPinned = Boolean([ ...remote ].find(([ , value ]) => value?.pinned) || local?.pinned);
171
+
163 172
     return {
164
-        _participantID: remoteParticipants[index]
173
+        _participantID: remoteParticipants[index],
174
+        _isAnyParticipantPinned
165 175
     };
166 176
 }
167 177
 

+ 69
- 0
react/features/filmstrip/components/web/VideoMenuTriggerButton.js Bestand weergeven

@@ -0,0 +1,69 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
6
+
7
+type Props = {
8
+
9
+    /**
10
+     * Hide popover callback.
11
+     */
12
+    hidePopover: Function,
13
+
14
+    /**
15
+     * Whether or not the button is for the local participant.
16
+     */
17
+    local: boolean,
18
+
19
+    /**
20
+     * The id of the participant for which the button is.
21
+     */
22
+    participantId?: string,
23
+
24
+    /**
25
+     * Whether popover is visible or not.
26
+     */
27
+    popoverVisible: boolean,
28
+
29
+    /**
30
+     * Show popover callback.
31
+     */
32
+    showPopover: Function,
33
+
34
+    /**
35
+     * Whether or not the component is visible.
36
+     */
37
+    visible: boolean
38
+}
39
+
40
+// eslint-disable-next-line no-confusing-arrow
41
+const VideoMenuTriggerButton = ({
42
+    hidePopover,
43
+    local,
44
+    participantId,
45
+    popoverVisible,
46
+    showPopover,
47
+    visible
48
+}: Props) => local
49
+    ? (
50
+        <span id = 'localvideomenu'>
51
+            <LocalVideoMenuTriggerButton
52
+                buttonVisible = { visible }
53
+                hidePopover = { hidePopover }
54
+                popoverVisible = { popoverVisible }
55
+                showPopover = { showPopover } />
56
+        </span>
57
+    )
58
+    : (
59
+        <span id = 'remotevideomenu'>
60
+            <RemoteVideoMenuTriggerButton
61
+                buttonVisible = { visible }
62
+                hidePopover = { hidePopover }
63
+                participantID = { participantId }
64
+                popoverVisible = { popoverVisible }
65
+                showPopover = { showPopover } />
66
+        </span>
67
+    );
68
+
69
+export default VideoMenuTriggerButton;

+ 0
- 43
react/features/filmstrip/components/web/VideoMutedIndicator.js Bestand weergeven

@@ -1,43 +0,0 @@
1
-/* @flow */
2
-
3
-import React, { Component } from 'react';
4
-
5
-import { IconCameraDisabled } from '../../../base/icons';
6
-import { BaseIndicator } from '../../../base/react';
7
-
8
-/**
9
- * The type of the React {@code Component} props of {@link VideoMutedIndicator}.
10
- */
11
-type Props = {
12
-
13
-    /**
14
-     * From which side of the indicator the tooltip should appear from.
15
-     */
16
-    tooltipPosition: string
17
-};
18
-
19
-/**
20
- * React {@code Component} for showing a video muted icon with a tooltip.
21
- *
22
- * @augments Component
23
- */
24
-class VideoMutedIndicator extends Component<Props> {
25
-    /**
26
-     * Implements React's {@link Component#render()}.
27
-     *
28
-     * @inheritdoc
29
-     */
30
-    render() {
31
-        return (
32
-            <BaseIndicator
33
-                className = 'videoMuted toolbar-icon'
34
-                icon = { IconCameraDisabled }
35
-                iconId = 'camera-disabled'
36
-                iconSize = { 13 }
37
-                tooltipKey = 'videothumbnail.videomute'
38
-                tooltipPosition = { this.props.tooltipPosition } />
39
-        );
40
-    }
41
-}
42
-
43
-export default VideoMutedIndicator;

+ 0
- 2
react/features/filmstrip/components/web/index.js Bestand weergeven

@@ -1,10 +1,8 @@
1 1
 // @flow
2 2
 
3 3
 export { default as AudioMutedIndicator } from './AudioMutedIndicator';
4
-export { default as DominantSpeakerIndicator } from './DominantSpeakerIndicator';
5 4
 export { default as Filmstrip } from './Filmstrip';
6 5
 export { default as ModeratorIndicator } from './ModeratorIndicator';
7 6
 export { default as RaisedHandIndicator } from './RaisedHandIndicator';
8 7
 export { default as StatusIndicators } from './StatusIndicators';
9
-export { default as VideoMutedIndicator } from './VideoMutedIndicator';
10 8
 export { default as Thumbnail } from './Thumbnail';

+ 33
- 44
react/features/filmstrip/constants.js Bestand weergeven

@@ -1,6 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import { BoxModel } from '../base/styles';
4
+import { LAYOUTS } from '../video-layout/constants';
4 5
 
5 6
 /**
6 7
  * The size (height and width) of the small (not tile view) thumbnails.
@@ -97,73 +98,43 @@ export const DISPLAY_VIDEO = 0;
97 98
 export const DISPLAY_AVATAR = 1;
98 99
 
99 100
 /**
100
- * Display mode constant used when neither video nor avatar is being displayed
101
- * on the small video. And we just show the display name.
101
+ * Maps the display modes to class name that will be applied on the thumbnail container.
102 102
  *
103
- * @type {number}
103
+ * @type {Array<string>}
104 104
  * @constant
105 105
  */
106
-export const DISPLAY_BLACKNESS_WITH_NAME = 2;
106
+export const DISPLAY_MODE_TO_CLASS_NAME = [
107
+    'display-video',
108
+    'display-avatar-only'
109
+];
107 110
 
108 111
 /**
109
- * Display mode constant used when video is displayed and display name
110
- * at the same time.
112
+ * The vertical margin of a tile.
111 113
  *
112 114
  * @type {number}
113
- * @constant
114 115
  */
115
-export const DISPLAY_VIDEO_WITH_NAME = 3;
116
+export const TILE_VERTICAL_MARGIN = 4;
116 117
 
117 118
 /**
118
- * Display mode constant used when neither video nor avatar is being displayed
119
- * on the small video. And we just show the display name.
119
+ * The horizontal margin of a tile.
120 120
  *
121 121
  * @type {number}
122
- * @constant
123
- */
124
-export const DISPLAY_AVATAR_WITH_NAME = 4;
125
-
126
-/**
127
- * Maps the display modes to class name that will be applied on the thumbnail container.
128
- *
129
- * @type {Array<string>}
130
- * @constant
131 122
  */
132
-export const DISPLAY_MODE_TO_CLASS_NAME = [
133
-    'display-video',
134
-    'display-avatar-only',
135
-    'display-name-on-black',
136
-    'display-name-on-video',
137
-    'display-avatar-with-name'
138
-];
139
-
140
-/**
141
- * Maps the display modes to string.
142
- *
143
- * @type {Array<string>}
144
- * @constant
145
- */
146
-export const DISPLAY_MODE_TO_STRING = [
147
-    'video',
148
-    'avatar',
149
-    'blackness-with-name',
150
-    'video-with-name',
151
-    'avatar-with-name'
152
-];
123
+export const TILE_HORIZONTAL_MARGIN = 4;
153 124
 
154 125
 /**
155
- * The vertical margin of a tile.
126
+ * The vertical margin of the tile grid container.
156 127
  *
157 128
  * @type {number}
158 129
  */
159
-export const TILE_VERTICAL_MARGIN = 4;
130
+export const TILE_VIEW_GRID_VERTICAL_MARGIN = 12;
160 131
 
161 132
 /**
162
- * The horizontal margin of a tile.
133
+ * The horizontal margin of the tile grid container.
163 134
  *
164 135
  * @type {number}
165 136
  */
166
-export const TILE_HORIZONTAL_MARGIN = 4;
137
+export const TILE_VIEW_GRID_HORIZONTAL_MARGIN = 12;
167 138
 
168 139
 /**
169 140
  * The height of the whole toolbar.
@@ -238,3 +209,21 @@ export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
238 209
  * @type {number}
239 210
  */
240 211
 export const TILE_MARGIN = 10;
212
+
213
+/**
214
+ * The popover position for the connection stats table.
215
+ */
216
+export const STATS_POPOVER_POSITION = {
217
+    [LAYOUTS.TILE_VIEW]: 'right-start',
218
+    [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left-start',
219
+    [LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top-end'
220
+};
221
+
222
+/**
223
+ * The tooltip position for the indicators on the thumbnail.
224
+ */
225
+export const INDICATORS_TOOLTIP_POSITION = {
226
+    [LAYOUTS.TILE_VIEW]: 'right',
227
+    [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left',
228
+    [LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top'
229
+};

+ 57
- 13
react/features/filmstrip/functions.web.js Bestand weergeven

@@ -15,20 +15,21 @@ import {
15 15
     isLocalTrackMuted,
16 16
     isRemoteTrackMuted
17 17
 } from '../base/tracks/functions';
18
+import { LAYOUTS } from '../video-layout';
18 19
 
19 20
 import {
20 21
     ASPECT_RATIO_BREAKPOINT,
21 22
     DISPLAY_AVATAR,
22
-    DISPLAY_AVATAR_WITH_NAME,
23
-    DISPLAY_BLACKNESS_WITH_NAME,
24 23
     DISPLAY_VIDEO,
25
-    DISPLAY_VIDEO_WITH_NAME,
24
+    INDICATORS_TOOLTIP_POSITION,
26 25
     SCROLL_SIZE,
27 26
     SQUARE_TILE_ASPECT_RATIO,
28 27
     STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
29 28
     TILE_ASPECT_RATIO,
30 29
     TILE_HORIZONTAL_MARGIN,
31 30
     TILE_VERTICAL_MARGIN,
31
+    TILE_VIEW_GRID_HORIZONTAL_MARGIN,
32
+    TILE_VIEW_GRID_VERTICAL_MARGIN,
32 33
     VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
33 34
 } from './constants';
34 35
 
@@ -190,8 +191,8 @@ export function calculateThumbnailSizeForTileView({
190 191
         aspectRatio = SQUARE_TILE_ASPECT_RATIO;
191 192
     }
192 193
 
193
-    const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN);
194
-    const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
194
+    const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN) - TILE_VIEW_GRID_HORIZONTAL_MARGIN;
195
+    const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
195 196
     const initialWidth = viewWidth / columns;
196 197
     const initialHeight = viewHeight / minVisibleRows;
197 198
     const aspectRatioHeight = initialWidth / aspectRatio;
@@ -240,32 +241,75 @@ export function getVerticalFilmstripVisibleAreaWidth() {
240 241
 /**
241 242
  * Computes information that determine the display mode.
242 243
  *
243
- * @param {Object} input - Obejct containing all necessary information for determining the display mode for
244
+ * @param {Object} input - Object containing all necessary information for determining the display mode for
244 245
  * the thumbnail.
245
- * @returns {number} - One of <tt>DISPLAY_VIDEO</tt>, <tt>DISPLAY_AVATAR</tt> or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
246
+ * @returns {number} - One of <tt>DISPLAY_VIDEO</tt> or <tt>DISPLAY_AVATAR</tt>.
246 247
 */
247
-export function computeDisplayMode(input: Object) {
248
+export function computeDisplayModeFromInput(input: Object) {
248 249
     const {
249 250
         isAudioOnly,
250 251
         isCurrentlyOnLargeVideo,
251 252
         isScreenSharing,
252 253
         canPlayEventReceived,
253
-        isHovered,
254 254
         isRemoteParticipant,
255 255
         tileViewActive
256 256
     } = input;
257 257
     const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
258 258
 
259 259
     if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
260
-        return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
260
+        return DISPLAY_AVATAR;
261 261
     } else if (isCurrentlyOnLargeVideo && !tileViewActive) {
262 262
         // Display name is always and only displayed when user is on the stage
263
-        return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
263
+        return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_VIDEO : DISPLAY_AVATAR;
264 264
     } else if (adjustedIsVideoPlayable && !isAudioOnly) {
265 265
         // check hovering and change state to video with name
266
-        return isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
266
+        return DISPLAY_VIDEO;
267 267
     }
268 268
 
269 269
     // check hovering and change state to avatar with name
270
-    return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
270
+    return DISPLAY_AVATAR;
271
+}
272
+
273
+/**
274
+ * Extracts information for props and state needed to compute the display mode.
275
+ *
276
+ * @param {Object} props - The Thumbnail component's props.
277
+ * @param {Object} state - The Thumbnail component's state.
278
+ * @returns {Object}
279
+*/
280
+export function getDisplayModeInput(props: Object, state: Object) {
281
+    const {
282
+        _currentLayout,
283
+        _isAudioOnly,
284
+        _isCurrentlyOnLargeVideo,
285
+        _isScreenSharing,
286
+        _isVideoPlayable,
287
+        _participant,
288
+        _videoTrack
289
+    } = props;
290
+    const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
291
+    const { canPlayEventReceived } = state;
292
+
293
+    return {
294
+        isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
295
+        isAudioOnly: _isAudioOnly,
296
+        tileViewActive,
297
+        isVideoPlayable: _isVideoPlayable,
298
+        connectionStatus: _participant?.connectionStatus,
299
+        canPlayEventReceived,
300
+        videoStream: Boolean(_videoTrack),
301
+        isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
302
+        isScreenSharing: _isScreenSharing,
303
+        videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
304
+    };
305
+}
306
+
307
+/**
308
+ * Gets the tooltip position for the thumbnail indicators.
309
+ *
310
+ * @param {string} currentLayout - The current layout of the app.
311
+ * @returns {string}
312
+ */
313
+export function getIndicatorsTooltipPosition(currentLayout: string) {
314
+    return INDICATORS_TOOLTIP_POSITION[currentLayout] || 'top';
271 315
 }

+ 15
- 447
react/features/participants-pane/components/web/MeetingParticipantContextMenu.js Bestand weergeven

@@ -1,87 +1,16 @@
1 1
 // @flow
2
-import { withStyles } from '@material-ui/styles';
3 2
 import React, { Component } from 'react';
4 3
 
5
-import { createBreakoutRoomsEvent, sendAnalytics } from '../../../analytics';
6
-import { approveParticipant } from '../../../av-moderation/actions';
7
-import { Avatar } from '../../../base/avatar';
8
-import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
9
-import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
10
-import { openDialog } from '../../../base/dialog';
11
-import { isIosMobileBrowser } from '../../../base/environment/utils';
12 4
 import { translate } from '../../../base/i18n';
13
-import {
14
-    IconCloseCircle,
15
-    IconCrown,
16
-    IconMessage,
17
-    IconMicDisabled,
18
-    IconMicrophone,
19
-    IconMuteEveryoneElse,
20
-    IconRingGroup,
21
-    IconShareVideo,
22
-    IconVideoOff
23
-} from '../../../base/icons';
24
-import { MEDIA_TYPE } from '../../../base/media';
25 5
 import {
26 6
     getLocalParticipant,
27
-    getParticipantByIdOrUndefined,
28
-    isLocalParticipantModerator,
29
-    isParticipantModerator
7
+    getParticipantByIdOrUndefined
30 8
 } from '../../../base/participants';
31 9
 import { connect } from '../../../base/redux';
32
-import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
33
-import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
34
-import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
35
-import { openChatById } from '../../../chat/actions';
36
-import { setVolume } from '../../../filmstrip/actions.web';
37
-import { stopSharedVideo } from '../../../shared-video/actions.any';
38
-import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
39
-import { VolumeSlider } from '../../../video-menu/components/web';
40
-import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
41
-import { isForceMuted } from '../../functions';
10
+import ParticipantContextMenu from '../../../video-menu/components/web/ParticipantContextMenu';
42 11
 
43 12
 type Props = {
44 13
 
45
-    /**
46
-     * Whether or not the participant is audio force muted.
47
-     */
48
-    _isAudioForceMuted: boolean,
49
-
50
-    /**
51
-     * The id of the current room.
52
-     */
53
-    _currentRoomId: String,
54
-
55
-    /**
56
-     * True if the local participant is moderator and false otherwise.
57
-     */
58
-    _isLocalModerator: boolean,
59
-
60
-    /**
61
-     * True if the chat button is enabled and false otherwise.
62
-     */
63
-    _isChatButtonEnabled: boolean,
64
-
65
-    /**
66
-     * True if the participant is moderator and false otherwise.
67
-     */
68
-    _isParticipantModerator: boolean,
69
-
70
-    /**
71
-     * True if the participant is video muted and false otherwise.
72
-     */
73
-    _isParticipantVideoMuted: boolean,
74
-
75
-    /**
76
-     * True if the participant is audio muted and false otherwise.
77
-     */
78
-    _isParticipantAudioMuted: boolean,
79
-
80
-    /**
81
-     * Whether or not the participant is video force muted.
82
-     */
83
-    _isVideoForceMuted: boolean,
84
-
85 14
     /**
86 15
      * Shared video local participant owner.
87 16
      */
@@ -92,27 +21,11 @@ type Props = {
92 21
      */
93 22
     _participant: Object,
94 23
 
95
-    /**
96
-     * Rooms reference.
97
-     */
98
-    _rooms: Array<Object>,
99
-
100
-    /**
101
-     * A value between 0 and 1 indicating the volume of the participant's
102
-     * audio element.
103
-     */
104
-    _volume: ?number,
105
-
106 24
     /**
107 25
      * Closes a drawer if open.
108 26
      */
109 27
     closeDrawer: Function,
110 28
 
111
-    /**
112
-     * An object containing the CSS classes.
113
-     */
114
-    classes?: {[ key: string]: string},
115
-
116 29
     /**
117 30
      * The dispatch function from redux.
118 31
      */
@@ -124,11 +37,6 @@ type Props = {
124 37
      */
125 38
     drawerParticipant: Object,
126 39
 
127
-    /**
128
-     * Callback used to open a confirmation dialog for audio muting.
129
-     */
130
-    muteAudio: Function,
131
-
132 40
     /**
133 41
      * Target elements against which positioning calculations are made.
134 42
      */
@@ -152,31 +60,7 @@ type Props = {
152 60
     /**
153 61
      * The ID of the participant.
154 62
      */
155
-    participantID?: string,
156
-
157
-    /**
158
-     * True if an overflow drawer should be displayed.
159
-     */
160
-    overflowDrawer: boolean,
161
-
162
-    /**
163
-     * The translate function.
164
-     */
165
-    t: Function
166
-};
167
-
168
-const styles = theme => {
169
-    return {
170
-        text: {
171
-            color: theme.palette.text02,
172
-            padding: '10px 16px',
173
-            height: '40px',
174
-            overflow: 'hidden',
175
-            display: 'flex',
176
-            alignItems: 'center',
177
-            boxSizing: 'border-box'
178
-        }
179
-    };
63
+    participantID: string
180 64
 };
181 65
 
182 66
 /**
@@ -184,166 +68,6 @@ const styles = theme => {
184 68
  */
185 69
 class MeetingParticipantContextMenu extends Component<Props> {
186 70
 
187
-    /**
188
-     * Creates new instance of MeetingParticipantContextMenu.
189
-     *
190
-     * @param {Props} props - The props.
191
-     */
192
-    constructor(props: Props) {
193
-        super(props);
194
-
195
-        this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this);
196
-        this._onGrantModerator = this._onGrantModerator.bind(this);
197
-        this._onKick = this._onKick.bind(this);
198
-        this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
199
-        this._onMuteVideo = this._onMuteVideo.bind(this);
200
-        this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
201
-        this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
202
-        this._onSendToRoom = this._onSendToRoom.bind(this);
203
-        this._onVolumeChange = this._onVolumeChange.bind(this);
204
-        this._onAskToUnmute = this._onAskToUnmute.bind(this);
205
-    }
206
-
207
-    _getCurrentParticipantId: () => string;
208
-
209
-    /**
210
-     * Returns the participant id for the item we want to operate.
211
-     *
212
-     * @returns {void}
213
-     */
214
-    _getCurrentParticipantId() {
215
-        const { _participant, drawerParticipant, overflowDrawer } = this.props;
216
-
217
-        return overflowDrawer ? drawerParticipant?.participantID : _participant?.id;
218
-    }
219
-
220
-    _onGrantModerator: () => void;
221
-
222
-    /**
223
-     * Grant moderator permissions.
224
-     *
225
-     * @returns {void}
226
-     */
227
-    _onGrantModerator() {
228
-        this.props.dispatch(openDialog(GrantModeratorDialog, {
229
-            participantID: this._getCurrentParticipantId()
230
-        }));
231
-    }
232
-
233
-    _onKick: () => void;
234
-
235
-    /**
236
-     * Kicks the participant.
237
-     *
238
-     * @returns {void}
239
-     */
240
-    _onKick() {
241
-        this.props.dispatch(openDialog(KickRemoteParticipantDialog, {
242
-            participantID: this._getCurrentParticipantId()
243
-        }));
244
-    }
245
-
246
-    _onStopSharedVideo: () => void;
247
-
248
-    /**
249
-     * Stops shared video.
250
-     *
251
-     * @returns {void}
252
-     */
253
-    _onStopSharedVideo() {
254
-        const { dispatch, onSelect } = this.props;
255
-
256
-        onSelect(true);
257
-        dispatch(stopSharedVideo());
258
-    }
259
-
260
-    _onMuteEveryoneElse: () => void;
261
-
262
-    /**
263
-     * Mutes everyone else.
264
-     *
265
-     * @returns {void}
266
-     */
267
-    _onMuteEveryoneElse() {
268
-        this.props.dispatch(openDialog(MuteEveryoneDialog, {
269
-            exclude: [ this._getCurrentParticipantId() ]
270
-        }));
271
-    }
272
-
273
-    _onMuteVideo: () => void;
274
-
275
-    /**
276
-     * Mutes the video of the selected participant.
277
-     *
278
-     * @returns {void}
279
-     */
280
-    _onMuteVideo() {
281
-        this.props.dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
282
-            participantID: this._getCurrentParticipantId()
283
-        }));
284
-    }
285
-
286
-    _onSendPrivateMessage: () => void;
287
-
288
-    /**
289
-     * Sends private message.
290
-     *
291
-     * @returns {void}
292
-     */
293
-    _onSendPrivateMessage() {
294
-        const { dispatch } = this.props;
295
-
296
-        dispatch(openChatById(this._getCurrentParticipantId()));
297
-    }
298
-
299
-    _onSendToRoom: (room: Object) => void;
300
-
301
-    /**
302
-     * Sends a participant to a room.
303
-     *
304
-     * @param {Object} room - The room that the participant should be moved to.
305
-     * @returns {void}
306
-     */
307
-    _onSendToRoom(room: Object) {
308
-        return () => {
309
-            const { _participant, dispatch, onSelect } = this.props;
310
-
311
-            onSelect(true);
312
-            sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
313
-            dispatch(sendParticipantToRoom(_participant.id, room.id));
314
-        };
315
-    }
316
-
317
-    _onVolumeChange: (number) => void;
318
-
319
-    /**
320
-     * Handles volume changes.
321
-     *
322
-     * @param {number} value - The new value for the volume.
323
-     * @returns {void}
324
-     */
325
-    _onVolumeChange(value) {
326
-        const { _participant, dispatch } = this.props;
327
-        const { id } = _participant;
328
-
329
-        dispatch(setVolume(id, value));
330
-    }
331
-
332
-    _onAskToUnmute: () => void;
333
-
334
-    /**
335
-     * Handles click on ask to unmute.
336
-     *
337
-     * @returns {void}
338
-     */
339
-    _onAskToUnmute() {
340
-        const { _participant, dispatch } = this.props;
341
-        const { id } = _participant;
342
-
343
-        dispatch(approveParticipant(id));
344
-    }
345
-
346
-
347 71
     /**
348 72
      * Implements React's {@link Component#render()}.
349 73
      *
@@ -352,165 +76,31 @@ class MeetingParticipantContextMenu extends Component<Props> {
352 76
      */
353 77
     render() {
354 78
         const {
355
-            _isAudioForceMuted,
356
-            _currentRoomId,
357
-            _isLocalModerator,
358
-            _isChatButtonEnabled,
359
-            _isParticipantModerator,
360
-            _isParticipantVideoMuted,
361
-            _isParticipantAudioMuted,
362
-            _isVideoForceMuted,
363 79
             _localVideoOwner,
364 80
             _participant,
365
-            _rooms,
366
-            _volume = 1,
367
-            classes,
368 81
             closeDrawer,
369 82
             drawerParticipant,
370 83
             offsetTarget,
371 84
             onEnter,
372 85
             onLeave,
373
-            onSelect,
374
-            overflowDrawer,
375
-            muteAudio,
376
-            t
86
+            onSelect
377 87
         } = this.props;
378 88
 
379 89
         if (!_participant) {
380 90
             return null;
381 91
         }
382 92
 
383
-        const showVolumeSlider = !isIosMobileBrowser()
384
-            && overflowDrawer
385
-            && typeof _volume === 'number'
386
-            && !isNaN(_volume);
387
-
388
-        const fakeParticipantActions = [ {
389
-            accessibilityLabel: t('toolbar.stopSharedVideo'),
390
-            icon: IconShareVideo,
391
-            onClick: this._onStopSharedVideo,
392
-            text: t('toolbar.stopSharedVideo')
393
-        } ];
394
-
395
-        const moderatorActions1 = [
396
-            overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted) ? {
397
-                accessibilityLabel: t(_isAudioForceMuted
398
-                    ? 'participantsPane.actions.askUnmute'
399
-                    : 'participantsPane.actions.allowVideo'),
400
-                icon: IconMicrophone,
401
-                onClick: this._onAskToUnmute,
402
-                text: t(_isAudioForceMuted
403
-                    ? 'participantsPane.actions.askUnmute'
404
-                    : 'participantsPane.actions.allowVideo')
405
-            } : null,
406
-            !_isParticipantAudioMuted && overflowDrawer ? {
407
-                accessibilityLabel: t('dialog.muteParticipantButton'),
408
-                icon: IconMicDisabled,
409
-                onClick: muteAudio(_participant),
410
-                text: t('dialog.muteParticipantButton')
411
-            } : null, {
412
-                accessibilityLabel: t('toolbar.accessibilityLabel.muteEveryoneElse'),
413
-                icon: IconMuteEveryoneElse,
414
-                onClick: this._onMuteEveryoneElse,
415
-                text: t('toolbar.accessibilityLabel.muteEveryoneElse')
416
-            },
417
-            _isParticipantVideoMuted ? null : {
418
-                accessibilityLabel: t('participantsPane.actions.stopVideo'),
419
-                icon: IconVideoOff,
420
-                onClick: this._onMuteVideo,
421
-                text: t('participantsPane.actions.stopVideo')
422
-            }
423
-        ].filter(Boolean);
424
-
425
-        const moderatorActions2 = [
426
-            _isLocalModerator && !_isParticipantModerator ? {
427
-                accessibilityLabel: t('toolbar.accessibilityLabel.grantModerator'),
428
-                icon: IconCrown,
429
-                onClick: this._onGrantModerator,
430
-                text: t('toolbar.accessibilityLabel.grantModerator')
431
-            } : null,
432
-            _isLocalModerator ? {
433
-                accessibilityLabel: t('videothumbnail.kick'),
434
-                icon: IconCloseCircle,
435
-                onClick: this._onKick,
436
-                text: t('videothumbnail.kick')
437
-            } : null,
438
-            _isChatButtonEnabled ? {
439
-                accessibilityLabel: t('toolbar.accessibilityLabel.privateMessage'),
440
-                icon: IconMessage,
441
-                onClick: this._onSendPrivateMessage,
442
-                text: t('toolbar.accessibilityLabel.privateMessage')
443
-            } : null
444
-        ].filter(Boolean);
445
-
446
-        const breakoutRoomActions = _rooms.map(room => {
447
-            if (room.id !== _currentRoomId) {
448
-                return {
449
-                    accessibilityLabel: room.name || t('breakoutRooms.mainRoom'),
450
-                    icon: IconRingGroup,
451
-                    onClick: this._onSendToRoom(room),
452
-                    text: room.name || t('breakoutRooms.mainRoom')
453
-                };
454
-            }
455
-
456
-            return null;
457
-        }
458
-        ).filter(Boolean);
459
-
460
-        const actions
461
-            = _participant?.isFakeParticipant ? (
462
-                <>
463
-                    {_localVideoOwner && (
464
-                        <ContextMenuItemGroup
465
-                            actions = { fakeParticipantActions } />
466
-                    )}
467
-                </>
468
-            ) : (
469
-                <>
470
-                    {_isLocalModerator
471
-                        && <ContextMenuItemGroup actions = { moderatorActions1 } />
472
-                    }
473
-
474
-                    <ContextMenuItemGroup actions = { moderatorActions2 } />
475
-
476
-                    {
477
-                        _isLocalModerator && _rooms.length > 1
478
-                            && <ContextMenuItemGroup actions = { breakoutRoomActions } >
479
-                                <div className = { classes && classes.text }>
480
-                                    {t('breakoutRooms.actions.sendToBreakoutRoom')}
481
-                                </div>
482
-                            </ContextMenuItemGroup>
483
-                    }
484
-                    { showVolumeSlider
485
-                        && <ContextMenuItemGroup>
486
-                            <VolumeSlider
487
-                                initialValue = { _volume }
488
-                                key = 'volume-slider'
489
-                                onChange = { this._onVolumeChange } />
490
-                        </ContextMenuItemGroup>
491
-                    }
492
-                </>
493
-            );
494
-
495 93
         return (
496
-            <ContextMenu
497
-                entity = { _participant }
498
-                isDrawerOpen = { drawerParticipant }
94
+            <ParticipantContextMenu
95
+                closeDrawer = { closeDrawer }
96
+                drawerParticipant = { drawerParticipant }
97
+                localVideoOwner = { _localVideoOwner }
499 98
                 offsetTarget = { offsetTarget }
500
-                onClick = { onSelect }
501
-                onDrawerClose = { closeDrawer }
502
-                onMouseEnter = { onEnter }
503
-                onMouseLeave = { onLeave }>
504
-                {overflowDrawer && <ContextMenuItemGroup
505
-                    actions = { [ {
506
-                        accessibilityLabel: drawerParticipant && drawerParticipant.displayName,
507
-                        customIcon: <Avatar
508
-                            participantId = { drawerParticipant && drawerParticipant.participantID }
509
-                            size = { 20 } />,
510
-                        text: drawerParticipant && drawerParticipant.displayName
511
-                    } ] } />}
512
-                {actions}
513
-            </ContextMenu>
99
+                onEnter = { onEnter }
100
+                onLeave = { onLeave }
101
+                onSelect = { onSelect }
102
+                participant = { _participant }
103
+                thumbnailMenu = { false } />
514 104
         );
515 105
     }
516 106
 }
@@ -531,32 +121,10 @@ function _mapStateToProps(state, ownProps): Object {
531 121
     const participant = getParticipantByIdOrUndefined(state,
532 122
         overflowDrawer ? drawerParticipant?.participantID : participantID);
533 123
 
534
-    const _currentRoomId = getCurrentRoomId(state);
535
-    const _isLocalModerator = isLocalParticipantModerator(state);
536
-    const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
537
-    const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
538
-    const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
539
-    const _isParticipantModerator = isParticipantModerator(participant);
540
-    const _rooms = Object.values(getBreakoutRooms(state));
541
-
542
-    const { participantsVolume } = state['features/filmstrip'];
543
-    const id = participant?.id;
544
-    const isLocal = participant?.local ?? true;
545
-
546 124
     return {
547
-        _isAudioForceMuted: isForceMuted(participant, MEDIA_TYPE.AUDIO, state),
548
-        _currentRoomId,
549
-        _isLocalModerator,
550
-        _isChatButtonEnabled,
551
-        _isParticipantModerator,
552
-        _isParticipantVideoMuted,
553
-        _isParticipantAudioMuted,
554
-        _isVideoForceMuted: isForceMuted(participant, MEDIA_TYPE.VIDEO, state),
555 125
         _localVideoOwner: Boolean(ownerId === localParticipantId),
556
-        _participant: participant,
557
-        _rooms,
558
-        _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined
126
+        _participant: participant
559 127
     };
560 128
 }
561 129
 
562
-export default translate(connect(_mapStateToProps)(withStyles(styles)(MeetingParticipantContextMenu)));
130
+export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));

+ 1
- 1
react/features/participants-pane/components/web/RaisedHandIndicator.js Bestand weergeven

@@ -8,7 +8,7 @@ import { Icon, IconRaisedHandHollow } from '../../../base/icons';
8 8
 const useStyles = makeStyles(theme => {
9 9
     return {
10 10
         indicator: {
11
-            backgroundColor: theme.palette.warning02,
11
+            backgroundColor: theme.palette.warning01,
12 12
             borderRadius: `${theme.shape.borderRadius / 2}px`,
13 13
             height: '24px',
14 14
             width: '24px'

+ 2
- 0
react/features/participants-pane/constants.js Bestand weergeven

@@ -95,11 +95,13 @@ export const VideoStateIcons = {
95 95
     [MEDIA_STATE.FORCE_MUTED]: (
96 96
         <Icon
97 97
             color = '#E04757'
98
+            id = 'videoMuted'
98 99
             size = { 16 }
99 100
             src = { IconCameraEmptyDisabled } />
100 101
     ),
101 102
     [MEDIA_STATE.MUTED]: (
102 103
         <Icon
104
+            id = 'videoMuted'
103 105
             size = { 16 }
104 106
             src = { IconCameraEmptyDisabled } />
105 107
     ),

+ 1
- 1
react/features/toolbox/components/MuteEveryonesVideoButton.js Bestand weergeven

@@ -27,7 +27,7 @@ type Props = AbstractButtonProps & {
27 27
  * every participant (except the local one).
28 28
  */
29 29
 class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
30
-    accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo';
30
+    accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideoStream';
31 31
     icon = IconMuteVideoEveryone;
32 32
     label = 'toolbar.muteEveryonesVideo';
33 33
     tooltip = 'toolbar.muteEveryonesVideo';

+ 3
- 1
react/features/video-menu/components/AbstractMuteButton.js Bestand weergeven

@@ -4,6 +4,7 @@ import {
4 4
     createRemoteVideoMenuButtonEvent,
5 5
     sendAnalytics
6 6
 } from '../../analytics';
7
+import { rejectParticipantAudio } from '../../av-moderation/actions';
7 8
 import { IconMicDisabled } from '../../base/icons';
8 9
 import { MEDIA_TYPE } from '../../base/media';
9 10
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
@@ -54,12 +55,13 @@ export default class AbstractMuteButton extends AbstractButton<Props, *> {
54 55
         const { dispatch, participantID } = this.props;
55 56
 
56 57
         sendAnalytics(createRemoteVideoMenuButtonEvent(
57
-            'mute.button',
58
+            'mute',
58 59
             {
59 60
                 'participant_id': participantID
60 61
             }));
61 62
 
62 63
         dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
64
+        dispatch(rejectParticipantAudio(participantID));
63 65
     }
64 66
 
65 67
     /**

+ 1
- 1
react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js Bestand weergeven

@@ -29,7 +29,7 @@ export type Props = AbstractButtonProps & {
29 29
  * An abstract remote video menu button which disables the camera of all the other participants.
30 30
  */
31 31
 export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton<Props, *> {
32
-    accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo';
32
+    accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideoStream';
33 33
     icon = IconMuteVideoEveryone;
34 34
     label = 'videothumbnail.domuteVideoOfOthers';
35 35
 

+ 49
- 0
react/features/video-menu/components/web/AskToUnmuteButton.js Bestand weergeven

@@ -0,0 +1,49 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+import { useDispatch } from 'react-redux';
6
+
7
+import { approveParticipant } from '../../../av-moderation/actions';
8
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
9
+import { IconMicrophoneEmpty } from '../../../base/icons';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * Whether or not the participant is audio force muted.
15
+     */
16
+    isAudioForceMuted: boolean,
17
+
18
+    /**
19
+     * Whether or not the participant is video force muted.
20
+     */
21
+    isVideoForceMuted: boolean,
22
+
23
+    /**
24
+     * The ID for the participant on which the button will act.
25
+     */
26
+    participantID: string
27
+}
28
+
29
+const AskToUnmuteButton = ({ isAudioForceMuted, isVideoForceMuted, participantID }: Props) => {
30
+    const dispatch = useDispatch();
31
+    const { t } = useTranslation();
32
+    const _onClick = useCallback(() => {
33
+        dispatch(approveParticipant(participantID));
34
+    }, [ participantID ]);
35
+
36
+    const text = isAudioForceMuted || !isVideoForceMuted
37
+        ? t('participantsPane.actions.askUnmute')
38
+        : t('participantsPane.actions.allowVideo');
39
+
40
+    return (
41
+        <ContextMenuItem
42
+            accessibilityLabel = { text }
43
+            icon = { IconMicrophoneEmpty }
44
+            onClick = { _onClick }
45
+            text = { text } />
46
+    );
47
+};
48
+
49
+export default AskToUnmuteButton;

+ 7
- 8
react/features/video-menu/components/web/ConnectionStatusButton.js Bestand weergeven

@@ -1,13 +1,12 @@
1 1
 // @flow
2 2
 import React, { useCallback } from 'react';
3 3
 
4
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
4 5
 import { translate } from '../../../base/i18n';
5 6
 import { IconInfo } from '../../../base/icons';
6 7
 import { connect } from '../../../base/redux';
7 8
 import { renderConnectionStatus } from '../../actions.web';
8 9
 
9
-import VideoMenuButton from './VideoMenuButton';
10
-
11 10
 type Props = {
12 11
 
13 12
     /**
@@ -29,19 +28,19 @@ type Props = {
29 28
 
30 29
 const ConnectionStatusButton = ({
31 30
     dispatch,
32
-    participantId,
33 31
     t
34 32
 }: Props) => {
35
-    const onClick = useCallback(() => {
33
+    const onClick = useCallback(e => {
34
+        e.stopPropagation();
36 35
         dispatch(renderConnectionStatus(true));
37 36
     }, [ dispatch ]);
38 37
 
39 38
     return (
40
-        <VideoMenuButton
41
-            buttonText = { t('videothumbnail.connectionInfo') }
39
+        <ContextMenuItem
40
+            accessibilityLabel = { t('videothumbnail.connectionInfo') }
42 41
             icon = { IconInfo }
43
-            id = { `connstatus_${participantId}` }
44
-            onClick = { onClick } />
42
+            onClick = { onClick }
43
+            text = { t('videothumbnail.connectionInfo') } />
45 44
     );
46 45
 };
47 46
 

+ 13
- 6
react/features/video-menu/components/web/FlipLocalVideoButton.js Bestand weergeven

@@ -2,12 +2,11 @@
2 2
 
3 3
 import React, { PureComponent } from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6 7
 import { connect } from '../../../base/redux';
7 8
 import { updateSettings } from '../../../base/settings';
8 9
 
9
-import VideoMenuButton from './VideoMenuButton';
10
-
11 10
 /**
12 11
  * The type of the React {@code Component} props of {@link FlipLocalVideoButton}.
13 12
  */
@@ -18,6 +17,11 @@ type Props = {
18 17
      */
19 18
     _localFlipX: boolean,
20 19
 
20
+    /**
21
+     * Button text class name.
22
+     */
23
+    className: string,
24
+
21 25
     /**
22 26
      * The redux dispatch function.
23 27
      */
@@ -61,15 +65,18 @@ class FlipLocalVideoButton extends PureComponent<Props> {
61 65
      */
62 66
     render() {
63 67
         const {
68
+            className,
64 69
             t
65 70
         } = this.props;
66 71
 
67 72
         return (
68
-            <VideoMenuButton
69
-                buttonText = { t('videothumbnail.flip') }
70
-                displayClass = 'fliplink'
73
+            <ContextMenuItem
74
+                accessibilityLabel = { t('videothumbnail.flip') }
75
+                className = 'fliplink'
71 76
                 id = 'flipLocalVideoButton'
72
-                onClick = { this._onClick } />
77
+                onClick = { this._onClick }
78
+                text = { t('videothumbnail.flip') }
79
+                textClassName = { className } />
73 80
         );
74 81
     }
75 82
 

+ 7
- 8
react/features/video-menu/components/web/GrantModeratorButton.js Bestand weergeven

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6 7
 import { IconCrown } from '../../../base/icons';
7 8
 import { connect } from '../../../base/redux';
@@ -10,8 +11,6 @@ import AbstractGrantModeratorButton, {
10 11
     type Props
11 12
 } from '../AbstractGrantModeratorButton';
12 13
 
13
-import VideoMenuButton from './VideoMenuButton';
14
-
15 14
 declare var interfaceConfig: Object;
16 15
 
17 16
 /**
@@ -37,20 +36,20 @@ class GrantModeratorButton extends AbstractGrantModeratorButton {
37 36
      * @returns {ReactElement}
38 37
      */
39 38
     render() {
40
-        const { participantID, t, visible } = this.props;
39
+        const { t, visible } = this.props;
41 40
 
42 41
         if (!visible) {
43 42
             return null;
44 43
         }
45 44
 
46 45
         return (
47
-            <VideoMenuButton
48
-                buttonText = { t('videothumbnail.grantModerator') }
49
-                displayClass = 'grantmoderatorlink'
46
+            <ContextMenuItem
47
+                accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
48
+                className = 'grantmoderatorlink'
50 49
                 icon = { IconCrown }
51
-                id = { `grantmoderatorlink_${participantID}` }
52 50
                 // eslint-disable-next-line react/jsx-handler-names
53
-                onClick = { this._handleClick } />
51
+                onClick = { this._handleClick }
52
+                text = { t('videothumbnail.grantModerator') } />
54 53
         );
55 54
     }
56 55
 

+ 14
- 7
react/features/video-menu/components/web/HideSelfViewVideoButton.js Bestand weergeven

@@ -2,14 +2,13 @@
2 2
 
3 3
 import React, { PureComponent } from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6 7
 import { connect } from '../../../base/redux';
7 8
 import { updateSettings } from '../../../base/settings';
8 9
 import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../../../notifications';
9 10
 import { openSettingsDialog, SETTINGS_TABS } from '../../../settings';
10 11
 
11
-import VideoMenuButton from './VideoMenuButton';
12
-
13 12
 /**
14 13
  * The type of the React {@code Component} props of {@link HideSelfViewVideoButton}.
15 14
  */
@@ -25,6 +24,11 @@ type Props = {
25 24
      */
26 25
     dispatch: Function,
27 26
 
27
+    /**
28
+     * Button text class name.
29
+     */
30
+    className: string,
31
+
28 32
     /**
29 33
      * Click handler executed aside from the main action.
30 34
      */
@@ -63,15 +67,18 @@ class HideSelfViewVideoButton extends PureComponent<Props> {
63 67
      */
64 68
     render() {
65 69
         const {
70
+            className,
66 71
             t
67 72
         } = this.props;
68 73
 
69 74
         return (
70
-            <VideoMenuButton
71
-                buttonText = { t('videothumbnail.hideSelfView') }
72
-                displayClass = 'hideselflink'
73
-                id = 'hideselfviewbutton'
74
-                onClick = { this._onClick } />
75
+            <ContextMenuItem
76
+                accessibilityLabel = { t('videothumbnail.hideSelfView') }
77
+                className = 'hideselflink'
78
+                id = 'hideselfviewButton'
79
+                onClick = { this._onClick }
80
+                text = { t('videothumbnail.hideSelfView') }
81
+                textClassName = { className } />
75 82
         );
76 83
     }
77 84
 

+ 8
- 8
react/features/video-menu/components/web/KickButton.js Bestand weergeven

@@ -2,15 +2,14 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6
-import { IconKick } from '../../../base/icons';
7
+import { IconCloseCircle } from '../../../base/icons';
7 8
 import { connect } from '../../../base/redux';
8 9
 import AbstractKickButton, {
9 10
     type Props
10 11
 } from '../AbstractKickButton';
11 12
 
12
-import VideoMenuButton from './VideoMenuButton';
13
-
14 13
 /**
15 14
  * Implements a React {@link Component} which displays a button for kicking out
16 15
  * a participant from the conference.
@@ -43,13 +42,14 @@ class KickButton extends AbstractKickButton {
43 42
         const { participantID, t } = this.props;
44 43
 
45 44
         return (
46
-            <VideoMenuButton
47
-                buttonText = { t('videothumbnail.kick') }
48
-                displayClass = 'kicklink'
49
-                icon = { IconKick }
45
+            <ContextMenuItem
46
+                accessibilityLabel = { t('videothumbnail.kick') }
47
+                className = 'kicklink'
48
+                icon = { IconCloseCircle }
50 49
                 id = { `ejectlink_${participantID}` }
51 50
                 // eslint-disable-next-line react/jsx-handler-names
52
-                onClick = { this._handleClick } />
51
+                onClick = { this._handleClick }
52
+                text = { t('videothumbnail.kick') } />
53 53
         );
54 54
     }
55 55
 

+ 65
- 25
react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js Bestand weergeven

@@ -1,11 +1,14 @@
1 1
 // @flow
2 2
 
3
+import { withStyles } from '@material-ui/styles';
3 4
 import React, { Component } from 'react';
4 5
 import { batch } from 'react-redux';
5 6
 
7
+import ContextMenu from '../../../base/components/context-menu/ContextMenu';
8
+import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup';
6 9
 import { isMobileBrowser } from '../../../base/environment/utils';
7 10
 import { translate } from '../../../base/i18n';
8
-import { Icon, IconMenuThumb } from '../../../base/icons';
11
+import { Icon, IconHorizontalPoints } from '../../../base/icons';
9 12
 import {
10 13
     getLocalParticipant
11 14
 } from '../../../base/participants';
@@ -20,8 +23,6 @@ import { renderConnectionStatus } from '../../actions.web';
20 23
 import ConnectionStatusButton from './ConnectionStatusButton';
21 24
 import FlipLocalVideoButton from './FlipLocalVideoButton';
22 25
 import HideSelfViewVideoButton from './HideSelfViewVideoButton';
23
-import VideoMenu from './VideoMenu';
24
-
25 26
 
26 27
 /**
27 28
  * The type of the React {@code Component} props of
@@ -30,29 +31,34 @@ import VideoMenu from './VideoMenu';
30 31
 type Props = {
31 32
 
32 33
     /**
33
-     * The redux dispatch function.
34
+     * Whether or not the button should be visible.
35
+     */
36
+    buttonVisible: boolean,
37
+
38
+    /**
39
+     * An object containing the CSS classes.
34 40
      */
35
-     dispatch: Function,
41
+    classes: Object,
36 42
 
37 43
     /**
38
-     * Gets a ref to the current component instance.
44
+     * The redux dispatch function.
39 45
      */
40
-     getRef: Function,
46
+    dispatch: Function,
41 47
 
42 48
     /**
43 49
      * Hides popover.
44 50
      */
45
-     hidePopover: Function,
51
+    hidePopover: Function,
46 52
 
47 53
     /**
48 54
      * Whether the popover is visible or not.
49 55
      */
50
-     popoverVisible: boolean,
56
+    popoverVisible: boolean,
51 57
 
52 58
     /**
53 59
      * Shows popover.
54 60
      */
55
-     showPopover: Function,
61
+    showPopover: Function,
56 62
 
57 63
     /**
58 64
      * The id of the local participant.
@@ -87,6 +93,29 @@ type Props = {
87 93
     t: Function
88 94
 };
89 95
 
96
+const styles = theme => {
97
+    return {
98
+        triggerButton: {
99
+            backgroundColor: theme.palette.action01,
100
+            padding: '3px',
101
+            display: 'inline-block',
102
+            borderRadius: '4px'
103
+        },
104
+
105
+        contextMenu: {
106
+            position: 'relative',
107
+            marginTop: 0,
108
+            right: 'auto',
109
+            padding: '0',
110
+            minWidth: '200px'
111
+        },
112
+
113
+        flipText: {
114
+            marginLeft: '36px'
115
+        }
116
+    };
117
+};
118
+
90 119
 /**
91 120
  * React Component for displaying an icon associated with opening the
92 121
  * the video menu for the local participant.
@@ -122,6 +151,8 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
122 151
             _showConnectionInfo,
123 152
             _overflowDrawer,
124 153
             _showLocalVideoFlipButton,
154
+            buttonVisible,
155
+            classes,
125 156
             hidePopover,
126 157
             popoverVisible,
127 158
             t
@@ -130,13 +161,22 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
130 161
         const content = _showConnectionInfo
131 162
             ? <ConnectionIndicatorContent participantId = { _localParticipantId } />
132 163
             : (
133
-                <VideoMenu id = 'localVideoMenu'>
134
-                    <FlipLocalVideoButton onClick = { hidePopover } />
135
-                    <HideSelfViewVideoButton onClick = { hidePopover } />
136
-                    { isMobileBrowser()
137
-                            && <ConnectionStatusButton participantId = { _localParticipantId } />
138
-                    }
139
-                </VideoMenu>
164
+                <ContextMenu
165
+                    className = { classes.contextMenu }
166
+                    hidden = { false }
167
+                    inDrawer = { _overflowDrawer }>
168
+                    <ContextMenuItemGroup>
169
+                        <FlipLocalVideoButton
170
+                            className = { _overflowDrawer ? classes.flipText : '' }
171
+                            onClick = { hidePopover } />
172
+                        <HideSelfViewVideoButton
173
+                            className = { _overflowDrawer ? classes.flipText : '' }
174
+                            onClick = { hidePopover } />
175
+                        { isMobileBrowser()
176
+                                    && <ConnectionStatusButton participantId = { _localParticipantId } />
177
+                        }
178
+                    </ContextMenuItemGroup>
179
+                </ContextMenu>
140 180
             );
141 181
 
142 182
         return (
@@ -149,14 +189,14 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
149 189
                     overflowDrawer = { _overflowDrawer }
150 190
                     position = { _menuPosition }
151 191
                     visible = { popoverVisible }>
152
-                    {!_overflowDrawer && (
192
+                    {!_overflowDrawer && buttonVisible && (
153 193
                         <span
154
-                            className = 'popover-trigger local-video-menu-trigger'>
194
+                            className = { classes.triggerButton }
195
+                            role = 'button'>
155 196
                             {!isMobileBrowser() && <Icon
156 197
                                 ariaLabel = { t('dialog.localUserControls') }
157
-                                role = 'button'
158
-                                size = '1.4em'
159
-                                src = { IconMenuThumb }
198
+                                size = { 18 }
199
+                                src = { IconHorizontalPoints }
160 200
                                 tabIndex = { 0 }
161 201
                                 title = { t('dialog.localUserControls') } />
162 202
                             }
@@ -221,10 +261,10 @@ function _mapStateToProps(state) {
221 261
         _menuPosition = 'left-start';
222 262
         break;
223 263
     case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
224
-        _menuPosition = 'left-end';
264
+        _menuPosition = 'left-start';
225 265
         break;
226 266
     case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
227
-        _menuPosition = 'top';
267
+        _menuPosition = 'top-start';
228 268
         break;
229 269
     default:
230 270
         _menuPosition = 'auto';
@@ -239,4 +279,4 @@ function _mapStateToProps(state) {
239 279
     };
240 280
 }
241 281
 
242
-export default translate(connect(_mapStateToProps)(LocalVideoMenuTriggerButton));
282
+export default translate(connect(_mapStateToProps)(withStyles(styles)(LocalVideoMenuTriggerButton)));

+ 13
- 16
react/features/video-menu/components/web/MuteButton.js Bestand weergeven

@@ -2,15 +2,15 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6
-import { IconMicDisabled } from '../../../base/icons';
7
+import { IconMicrophoneEmptySlash } from '../../../base/icons';
7 8
 import { connect } from '../../../base/redux';
8 9
 import AbstractMuteButton, {
9 10
     _mapStateToProps,
10 11
     type Props
11 12
 } from '../AbstractMuteButton';
12 13
 
13
-import VideoMenuButton from './VideoMenuButton';
14 14
 
15 15
 /**
16 16
  * Implements a React {@link Component} which displays a button for audio muting
@@ -41,23 +41,20 @@ class MuteButton extends AbstractMuteButton {
41 41
      * @returns {ReactElement}
42 42
      */
43 43
     render() {
44
-        const { _audioTrackMuted, participantID, t } = this.props;
45
-        const muteConfig = _audioTrackMuted ? {
46
-            translationKey: 'videothumbnail.muted',
47
-            muteClassName: 'mutelink disabled'
48
-        } : {
49
-            translationKey: 'videothumbnail.domute',
50
-            muteClassName: 'mutelink'
51
-        };
44
+        const { _audioTrackMuted, t } = this.props;
45
+
46
+        if (_audioTrackMuted) {
47
+            return null;
48
+        }
52 49
 
53 50
         return (
54
-            <VideoMenuButton
55
-                buttonText = { t(muteConfig.translationKey) }
56
-                displayClass = { muteConfig.muteClassName }
57
-                icon = { IconMicDisabled }
58
-                id = { `mutelink_${participantID}` }
51
+            <ContextMenuItem
52
+                accessibilityLabel = { t('dialog.muteParticipantButton') }
53
+                className = 'mutelink'
54
+                icon = { IconMicrophoneEmptySlash }
59 55
                 // eslint-disable-next-line react/jsx-handler-names
60
-                onClick = { this._handleClick } />
56
+                onClick = { this._handleClick }
57
+                text = { t('dialog.muteParticipantButton') } />
61 58
         );
62 59
     }
63 60
 

+ 6
- 8
react/features/video-menu/components/web/MuteEveryoneElseButton.js Bestand weergeven

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6 7
 import { IconMuteEveryoneElse } from '../../../base/icons';
7 8
 import { connect } from '../../../base/redux';
@@ -9,8 +10,6 @@ import AbstractMuteEveryoneElseButton, {
9 10
     type Props
10 11
 } from '../AbstractMuteEveryoneElseButton';
11 12
 
12
-import VideoMenuButton from './VideoMenuButton';
13
-
14 13
 /**
15 14
  * Implements a React {@link Component} which displays a button for audio muting
16 15
  * every participant in the conference except the one with the given
@@ -35,16 +34,15 @@ class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
35 34
      * @returns {ReactElement}
36 35
      */
37 36
     render() {
38
-        const { participantID, t } = this.props;
37
+        const { t } = this.props;
39 38
 
40 39
         return (
41
-            <VideoMenuButton
42
-                buttonText = { t('videothumbnail.domuteOthers') }
43
-                displayClass = { 'mutelink' }
40
+            <ContextMenuItem
41
+                accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') }
44 42
                 icon = { IconMuteEveryoneElse }
45
-                id = { `mutelink_${participantID}` }
46 43
                 // eslint-disable-next-line react/jsx-handler-names
47
-                onClick = { this._handleClick } />
44
+                onClick = { this._handleClick }
45
+                text = { t('videothumbnail.domuteOthers') } />
48 46
         );
49 47
     }
50 48
 

+ 6
- 8
react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js Bestand weergeven

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6 7
 import { IconMuteVideoEveryoneElse } from '../../../base/icons';
7 8
 import { connect } from '../../../base/redux';
@@ -9,8 +10,6 @@ import AbstractMuteEveryoneElsesVideoButton, {
9 10
     type Props
10 11
 } from '../AbstractMuteEveryoneElsesVideoButton';
11 12
 
12
-import VideoMenuButton from './VideoMenuButton';
13
-
14 13
 /**
15 14
  * Implements a React {@link Component} which displays a button for audio muting
16 15
  * every participant in the conference except the one with the given
@@ -35,16 +34,15 @@ class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton
35 34
      * @returns {ReactElement}
36 35
      */
37 36
     render() {
38
-        const { participantID, t } = this.props;
37
+        const { t } = this.props;
39 38
 
40 39
         return (
41
-            <VideoMenuButton
42
-                buttonText = { t('videothumbnail.domuteVideoOfOthers') }
43
-                displayClass = { 'mutelink' }
40
+            <ContextMenuItem
41
+                accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') }
44 42
                 icon = { IconMuteVideoEveryoneElse }
45
-                id = { `mutelink_${participantID}` }
46 43
                 // eslint-disable-next-line react/jsx-handler-names
47
-                onClick = { this._handleClick } />
44
+                onClick = { this._handleClick }
45
+                text = { t('videothumbnail.domuteVideoOfOthers') } />
48 46
         );
49 47
     }
50 48
 

+ 13
- 17
react/features/video-menu/components/web/MuteVideoButton.js Bestand weergeven

@@ -2,16 +2,15 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6
-import { IconCameraDisabled } from '../../../base/icons';
7
+import { IconVideoOff } from '../../../base/icons';
7 8
 import { connect } from '../../../base/redux';
8 9
 import AbstractMuteVideoButton, {
9 10
     _mapStateToProps,
10 11
     type Props
11 12
 } from '../AbstractMuteVideoButton';
12 13
 
13
-import VideoMenuButton from './VideoMenuButton';
14
-
15 14
 /**
16 15
  * Implements a React {@link Component} which displays a button for disabling
17 16
  * the camera of a participant in the conference.
@@ -41,23 +40,20 @@ class MuteVideoButton extends AbstractMuteVideoButton {
41 40
      * @returns {ReactElement}
42 41
      */
43 42
     render() {
44
-        const { _videoTrackMuted, participantID, t } = this.props;
45
-        const muteConfig = _videoTrackMuted ? {
46
-            translationKey: 'videothumbnail.videoMuted',
47
-            muteClassName: 'mutevideolink disabled'
48
-        } : {
49
-            translationKey: 'videothumbnail.domuteVideo',
50
-            muteClassName: 'mutevideolink'
51
-        };
43
+        const { _videoTrackMuted, t } = this.props;
44
+
45
+        if (_videoTrackMuted) {
46
+            return null;
47
+        }
52 48
 
53 49
         return (
54
-            <VideoMenuButton
55
-                buttonText = { t(muteConfig.translationKey) }
56
-                displayClass = { muteConfig.muteClassName }
57
-                icon = { IconCameraDisabled }
58
-                id = { `mutelink_${participantID}` }
50
+            <ContextMenuItem
51
+                accessibilityLabel = { t('participantsPane.actions.stopVideo') }
52
+                className = 'mutevideolink'
53
+                icon = { IconVideoOff }
59 54
                 // eslint-disable-next-line react/jsx-handler-names
60
-                onClick = { this._handleClick } />
55
+                onClick = { this._handleClick }
56
+                text = { t('participantsPane.actions.stopVideo') } />
61 57
         );
62 58
     }
63 59
 

+ 332
- 0
react/features/video-menu/components/web/ParticipantContextMenu.js Bestand weergeven

@@ -0,0 +1,332 @@
1
+// @flow
2
+
3
+import { makeStyles } from '@material-ui/styles';
4
+import React, { useCallback } from 'react';
5
+import { useTranslation } from 'react-i18next';
6
+import { useDispatch, useSelector } from 'react-redux';
7
+
8
+import { Avatar } from '../../../base/avatar';
9
+import ContextMenu from '../../../base/components/context-menu/ContextMenu';
10
+import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup';
11
+import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
12
+import { IconShareVideo } from '../../../base/icons';
13
+import { MEDIA_TYPE } from '../../../base/media';
14
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
15
+import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
16
+import { setVolume } from '../../../filmstrip/actions.web';
17
+import { isForceMuted } from '../../../participants-pane/functions';
18
+import { requestRemoteControl, stopController } from '../../../remote-control';
19
+import { stopSharedVideo } from '../../../shared-video/actions.any';
20
+import { showOverflowDrawer } from '../../../toolbox/functions.web';
21
+
22
+import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
23
+import SendToRoomButton from './SendToRoomButton';
24
+
25
+import {
26
+    AskToUnmuteButton,
27
+    ConnectionStatusButton,
28
+    GrantModeratorButton,
29
+    MuteButton,
30
+    MuteEveryoneElseButton,
31
+    MuteEveryoneElsesVideoButton,
32
+    MuteVideoButton,
33
+    KickButton,
34
+    PrivateMessageMenuButton,
35
+    RemoteControlButton,
36
+    VolumeSlider
37
+} from './';
38
+
39
+type Props = {
40
+
41
+    /**
42
+     * Class name for the context menu.
43
+     */
44
+    className?: string,
45
+
46
+    /**
47
+     * Closes a drawer if open.
48
+     */
49
+    closeDrawer?: Function,
50
+
51
+    /**
52
+     * The participant for which the drawer is open.
53
+     * It contains the displayName & participantID.
54
+     */
55
+    drawerParticipant?: Object,
56
+
57
+    /**
58
+     * Shared video local participant owner.
59
+     */
60
+    localVideoOwner?: boolean,
61
+
62
+    /**
63
+     * Target elements against which positioning calculations are made.
64
+     */
65
+    offsetTarget?: HTMLElement,
66
+
67
+    /**
68
+     * Callback for the mouse entering the component.
69
+     */
70
+    onEnter?: Function,
71
+
72
+    /**
73
+     * Callback for the mouse leaving the component.
74
+     */
75
+    onLeave?: Function,
76
+
77
+    /**
78
+     * Callback for making a selection in the menu.
79
+     */
80
+    onSelect: Function,
81
+
82
+    /**
83
+     * Participant reference.
84
+     */
85
+    participant: Object,
86
+
87
+    /**
88
+     * The current state of the participant's remote control session.
89
+     */
90
+    remoteControlState?: number,
91
+
92
+    /**
93
+     * Whether or not the menu is displayed in the thumbnail remote video menu.
94
+     */
95
+    thumbnailMenu: ?boolean
96
+}
97
+
98
+const useStyles = makeStyles(theme => {
99
+    return {
100
+        text: {
101
+            color: theme.palette.text02,
102
+            padding: '10px 16px',
103
+            height: '40px',
104
+            overflow: 'hidden',
105
+            display: 'flex',
106
+            alignItems: 'center',
107
+            boxSizing: 'border-box'
108
+        }
109
+    };
110
+});
111
+
112
+const ParticipantContextMenu = ({
113
+    className,
114
+    closeDrawer,
115
+    drawerParticipant,
116
+    localVideoOwner,
117
+    offsetTarget,
118
+    onEnter,
119
+    onLeave,
120
+    onSelect,
121
+    participant,
122
+    remoteControlState,
123
+    thumbnailMenu
124
+}: Props) => {
125
+    const dispatch = useDispatch();
126
+    const { t } = useTranslation();
127
+    const styles = useStyles();
128
+
129
+    const localParticipant = useSelector(getLocalParticipant);
130
+    const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR);
131
+    const _isAudioForceMuted = useSelector(state =>
132
+        isForceMuted(participant, MEDIA_TYPE.AUDIO, state));
133
+    const _isVideoForceMuted = useSelector(state =>
134
+        isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
135
+    const _overflowDrawer = useSelector(showOverflowDrawer);
136
+    const { remoteVideoMenu = {}, disableRemoteMute, startSilent }
137
+        = useSelector(state => state['features/base/config']);
138
+    const { disableKick, disableGrantModerator } = remoteVideoMenu;
139
+    const { participantsVolume } = useSelector(state => state['features/filmstrip']);
140
+    const _volume = (participant?.local ?? true ? undefined
141
+        : participant?.id ? participantsVolume[participant?.id] : undefined) || 1;
142
+
143
+    const _currentRoomId = useSelector(getCurrentRoomId);
144
+    const _rooms = Object.values(useSelector(getBreakoutRooms));
145
+
146
+    const _onVolumeChange = useCallback(value => {
147
+        dispatch(setVolume(participant.id, value));
148
+    }, [ setVolume, dispatch ]);
149
+
150
+    const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
151
+
152
+    const _onStopSharedVideo = useCallback(() => {
153
+        clickHandler();
154
+        dispatch(stopSharedVideo());
155
+    }, [ stopSharedVideo ]);
156
+
157
+    const _getCurrentParticipantId = useCallback(() => {
158
+        const drawer = _overflowDrawer && !thumbnailMenu;
159
+
160
+        return (drawer ? drawerParticipant?.participantID : participant?.id) ?? '';
161
+    }
162
+    , [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
163
+
164
+    const buttons = [];
165
+    const buttons2 = [];
166
+
167
+    const showVolumeSlider = !startSilent
168
+        && !isIosMobileBrowser()
169
+        && (_overflowDrawer || thumbnailMenu)
170
+        && typeof _volume === 'number'
171
+        && !isNaN(_volume);
172
+
173
+    const fakeParticipantActions = [ {
174
+        accessibilityLabel: t('toolbar.stopSharedVideo'),
175
+        icon: IconShareVideo,
176
+        onClick: _onStopSharedVideo,
177
+        text: t('toolbar.stopSharedVideo')
178
+    } ];
179
+
180
+    if (_isModerator) {
181
+        if (thumbnailMenu || _overflowDrawer) {
182
+            buttons.push(<AskToUnmuteButton
183
+                isAudioForceMuted = { _isAudioForceMuted }
184
+                isVideoForceMuted = { _isVideoForceMuted }
185
+                key = 'ask-unmute'
186
+                participantID = { _getCurrentParticipantId() } />
187
+            );
188
+        }
189
+        if (!disableRemoteMute) {
190
+            buttons.push(
191
+                <MuteButton
192
+                    key = 'mute'
193
+                    participantID = { _getCurrentParticipantId() } />
194
+            );
195
+            buttons.push(
196
+                <MuteEveryoneElseButton
197
+                    key = 'mute-others'
198
+                    participantID = { _getCurrentParticipantId() } />
199
+            );
200
+            buttons.push(
201
+                <MuteVideoButton
202
+                    key = 'mute-video'
203
+                    participantID = { _getCurrentParticipantId() } />
204
+            );
205
+            buttons.push(
206
+                <MuteEveryoneElsesVideoButton
207
+                    key = 'mute-others-video'
208
+                    participantID = { _getCurrentParticipantId() } />
209
+            );
210
+        }
211
+
212
+        if (!disableGrantModerator) {
213
+            buttons2.push(
214
+                <GrantModeratorButton
215
+                    key = 'grant-moderator'
216
+                    participantID = { _getCurrentParticipantId() } />
217
+            );
218
+        }
219
+
220
+        if (!disableKick) {
221
+            buttons2.push(
222
+                <KickButton
223
+                    key = 'kick'
224
+                    participantID = { _getCurrentParticipantId() } />
225
+            );
226
+        }
227
+    }
228
+
229
+    buttons2.push(
230
+        <PrivateMessageMenuButton
231
+            key = 'privateMessage'
232
+            participantID = { _getCurrentParticipantId() } />
233
+    );
234
+
235
+    if (thumbnailMenu && isMobileBrowser()) {
236
+        buttons2.push(
237
+            <ConnectionStatusButton
238
+                key = 'conn-status'
239
+                participantId = { _getCurrentParticipantId() } />
240
+        );
241
+    }
242
+
243
+    if (thumbnailMenu && remoteControlState) {
244
+        let onRemoteControlToggle = null;
245
+
246
+        if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
247
+            onRemoteControlToggle = () => dispatch(stopController(true));
248
+        } else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
249
+            onRemoteControlToggle = () => dispatch(requestRemoteControl(_getCurrentParticipantId()));
250
+        }
251
+
252
+        buttons2.push(
253
+            <RemoteControlButton
254
+                key = 'remote-control'
255
+                onClick = { onRemoteControlToggle }
256
+                participantID = { _getCurrentParticipantId() }
257
+                remoteControlState = { remoteControlState } />
258
+        );
259
+    }
260
+
261
+    const breakoutRoomsButtons = [];
262
+
263
+    if (!thumbnailMenu && _isModerator) {
264
+        _rooms.forEach((room: Object) => {
265
+            if (room.id !== _currentRoomId) {
266
+                breakoutRoomsButtons.push(
267
+                    <SendToRoomButton
268
+                        key = { room.id }
269
+                        onClick = { clickHandler }
270
+                        participantID = { _getCurrentParticipantId() }
271
+                        room = { room } />
272
+                );
273
+            }
274
+        });
275
+    }
276
+
277
+    return (
278
+        <ContextMenu
279
+            className = { className }
280
+            entity = { participant }
281
+            hidden = { thumbnailMenu ? false : undefined }
282
+            inDrawer = { thumbnailMenu && _overflowDrawer }
283
+            isDrawerOpen = { drawerParticipant }
284
+            offsetTarget = { offsetTarget }
285
+            onClick = { onSelect }
286
+            onDrawerClose = { thumbnailMenu ? onSelect : closeDrawer }
287
+            onMouseEnter = { onEnter }
288
+            onMouseLeave = { onLeave }>
289
+            {!thumbnailMenu && _overflowDrawer && drawerParticipant && <ContextMenuItemGroup
290
+                actions = { [ {
291
+                    accessibilityLabel: drawerParticipant.displayName,
292
+                    customIcon: <Avatar
293
+                        participantId = { drawerParticipant.participantID }
294
+                        size = { 20 } />,
295
+                    text: drawerParticipant.displayName
296
+                } ] } />}
297
+            {participant?.isFakeParticipant ? localVideoOwner && (
298
+                <ContextMenuItemGroup
299
+                    actions = { fakeParticipantActions } />
300
+            ) : (
301
+                <>
302
+                    {buttons.length > 0 && (
303
+                        <ContextMenuItemGroup>
304
+                            {buttons}
305
+                        </ContextMenuItemGroup>
306
+                    )}
307
+                    <ContextMenuItemGroup>
308
+                        {buttons2}
309
+                    </ContextMenuItemGroup>
310
+                    {showVolumeSlider && (
311
+                        <ContextMenuItemGroup>
312
+                            <VolumeSlider
313
+                                initialValue = { _volume }
314
+                                key = 'volume-slider'
315
+                                onChange = { _onVolumeChange } />
316
+                        </ContextMenuItemGroup>
317
+                    )}
318
+                    {breakoutRoomsButtons.length > 0 && (
319
+                        <ContextMenuItemGroup>
320
+                            <div className = { styles.text }>
321
+                                {t('breakoutRooms.actions.sendToBreakoutRoom')}
322
+                            </div>
323
+                            {breakoutRoomsButtons}
324
+                        </ContextMenuItemGroup>
325
+                    )}
326
+                </>
327
+            )}
328
+        </ContextMenu>
329
+    );
330
+};
331
+
332
+export default ParticipantContextMenu;

+ 6
- 7
react/features/video-menu/components/web/PrivateMessageMenuButton.js Bestand weergeven

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
5 6
 import { translate } from '../../../base/i18n';
6 7
 import { IconMessage } from '../../../base/icons';
7 8
 import { connect } from '../../../base/redux';
@@ -12,8 +13,6 @@ import {
12 13
 } from '../../../chat/components/web/PrivateMessageButton';
13 14
 import { isButtonEnabled } from '../../../toolbox/functions.web';
14 15
 
15
-import VideoMenuButton from './VideoMenuButton';
16
-
17 16
 declare var interfaceConfig: Object;
18 17
 
19 18
 type Props = AbstractProps & {
@@ -49,18 +48,18 @@ class PrivateMessageMenuButton extends Component<Props> {
49 48
      * @returns {ReactElement}
50 49
      */
51 50
     render() {
52
-        const { participantID, t, _hidden } = this.props;
51
+        const { t, _hidden } = this.props;
53 52
 
54 53
         if (_hidden) {
55 54
             return null;
56 55
         }
57 56
 
58 57
         return (
59
-            <VideoMenuButton
60
-                buttonText = { t('toolbar.privateMessage') }
58
+            <ContextMenuItem
59
+                accessibilityLabel = { t('toolbar.accessibilityLabel.privateMessage') }
61 60
                 icon = { IconMessage }
62
-                id = { `privmsglink_${participantID}` }
63
-                onClick = { this._onClick } />
61
+                onClick = { this._onClick }
62
+                text = { t('toolbar.privateMessage') } />
64 63
         );
65 64
     }
66 65
 

+ 9
- 10
react/features/video-menu/components/web/RemoteControlButton.js Bestand weergeven

@@ -6,11 +6,10 @@ import {
6 6
     createRemoteVideoMenuButtonEvent,
7 7
     sendAnalytics
8 8
 } from '../../../analytics';
9
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
9 10
 import { translate } from '../../../base/i18n';
10 11
 import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons';
11 12
 
12
-import VideoMenuButton from './VideoMenuButton';
13
-
14 13
 // TODO: Move these enums into the store after further reactification of the
15 14
 // non-react RemoteVideo component.
16 15
 export const REMOTE_CONTROL_MENU_STATES = {
@@ -76,19 +75,18 @@ class RemoteControlButton extends Component<Props> {
76 75
      */
77 76
     render() {
78 77
         const {
79
-            participantID,
80 78
             remoteControlState,
81 79
             t
82 80
         } = this.props;
83 81
 
84
-        let className, icon;
82
+        let disabled = false, icon;
85 83
 
86 84
         switch (remoteControlState) {
87 85
         case REMOTE_CONTROL_MENU_STATES.NOT_STARTED:
88 86
             icon = IconRemoteControlStart;
89 87
             break;
90 88
         case REMOTE_CONTROL_MENU_STATES.REQUESTING:
91
-            className = ' disabled';
89
+            disabled = true;
92 90
             icon = IconRemoteControlStart;
93 91
             break;
94 92
         case REMOTE_CONTROL_MENU_STATES.STARTED:
@@ -102,12 +100,13 @@ class RemoteControlButton extends Component<Props> {
102 100
         }
103 101
 
104 102
         return (
105
-            <VideoMenuButton
106
-                buttonText = { t('videothumbnail.remoteControl') }
107
-                displayClass = { className }
103
+            <ContextMenuItem
104
+                accessibilityLabel = { t('videothumbnail.remoteControl') }
105
+                className = 'kicklink'
106
+                disabled = { disabled }
108 107
                 icon = { icon }
109
-                id = { `remoteControl_${participantID}` }
110
-                onClick = { this._onClick } />
108
+                onClick = { this._onClick }
109
+                text = { t('videothumbnail.remoteControl') } />
111 110
         );
112 111
     }
113 112
 

+ 58
- 193
react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js Bestand weergeven

@@ -1,39 +1,26 @@
1 1
 // @flow
2 2
 
3 3
 /* eslint-disable react/jsx-handler-names */
4
+import { withStyles } from '@material-ui/styles';
4 5
 import React, { Component } from 'react';
5 6
 import { batch } from 'react-redux';
6 7
 
7 8
 import ConnectionIndicatorContent from
8 9
     '../../../../features/connection-indicator/components/web/ConnectionIndicatorContent';
9
-import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
10
+import { isMobileBrowser } from '../../../base/environment/utils';
10 11
 import { translate } from '../../../base/i18n';
11
-import { Icon, IconMenuThumb } from '../../../base/icons';
12
-import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
12
+import { Icon, IconHorizontalPoints } from '../../../base/icons';
13
+import { getParticipantById } from '../../../base/participants';
13 14
 import { Popover } from '../../../base/popover';
14 15
 import { connect } from '../../../base/redux';
15 16
 import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
16
-import { requestRemoteControl, stopController } from '../../../remote-control';
17 17
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
18 18
 import { renderConnectionStatus } from '../../actions.web';
19 19
 
20
-import ConnectionStatusButton from './ConnectionStatusButton';
21
-import MuteEveryoneElseButton from './MuteEveryoneElseButton';
22
-import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
20
+import ParticipantContextMenu from './ParticipantContextMenu';
23 21
 import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
24 22
 
25 23
 
26
-import {
27
-    GrantModeratorButton,
28
-    MuteButton,
29
-    MuteVideoButton,
30
-    KickButton,
31
-    PrivateMessageMenuButton,
32
-    RemoteControlButton,
33
-    VideoMenu,
34
-    VolumeSlider
35
-} from './';
36
-
37 24
 declare var $: Object;
38 25
 
39 26
 /**
@@ -45,37 +32,17 @@ type Props = {
45 32
     /**
46 33
      * Hides popover.
47 34
      */
48
-     hidePopover: Function,
35
+    hidePopover: Function,
49 36
 
50 37
     /**
51 38
      * Whether the popover is visible or not.
52 39
      */
53
-     popoverVisible: boolean,
40
+    popoverVisible: boolean,
54 41
 
55 42
     /**
56 43
      * Shows popover.
57 44
      */
58
-     showPopover: Function,
59
-
60
-    /**
61
-     * Whether or not to display the kick button.
62
-     */
63
-    _disableKick: boolean,
64
-
65
-    /**
66
-     * Whether or not to display the remote mute buttons.
67
-     */
68
-    _disableRemoteMute: Boolean,
69
-
70
-    /**
71
-     * Whether or not to display the grant moderator button.
72
-     */
73
-    _disableGrantModerator: Boolean,
74
-
75
-    /**
76
-     * Whether or not the participant is a conference moderator.
77
-     */
78
-    _isModerator: boolean,
45
+    showPopover: Function,
79 46
 
80 47
     /**
81 48
      * The position relative to the trigger the remote menu should display
@@ -90,31 +57,29 @@ type Props = {
90 57
     _overflowDrawer: boolean,
91 58
 
92 59
     /**
93
-     * The current state of the participant's remote control session.
60
+     * Participant reference.
94 61
      */
95
-    _remoteControlState: number,
62
+    _participant: Object,
96 63
 
97 64
     /**
98
-     * The redux dispatch function.
65
+     * The current state of the participant's remote control session.
99 66
      */
100
-    dispatch: Function,
67
+    _remoteControlState: number,
101 68
 
102 69
     /**
103
-     * Gets a ref to the current component instance.
70
+     * Whether or not the button should be visible.
104 71
      */
105
-    getRef: Function,
72
+    buttonVisible: boolean,
106 73
 
107 74
     /**
108
-     * A value between 0 and 1 indicating the volume of the participant's
109
-     * audio element.
75
+     * An object containing the CSS classes.
110 76
      */
111
-    initialVolumeValue: ?number,
77
+    classes: Object,
112 78
 
113 79
     /**
114
-     * Callback to invoke when changing the level of the participant's
115
-     * audio element.
80
+     * The redux dispatch function.
116 81
      */
117
-    onVolumeChange: Function,
82
+    dispatch: Function,
118 83
 
119 84
     /**
120 85
      * The ID for the participant on which the remote video menu will act.
@@ -137,6 +102,26 @@ type Props = {
137 102
     t: Function
138 103
 };
139 104
 
105
+const styles = theme => {
106
+    return {
107
+        triggerButton: {
108
+            backgroundColor: theme.palette.action01,
109
+            padding: '3px',
110
+            display: 'inline-block',
111
+            borderRadius: '4px'
112
+        },
113
+
114
+        contextMenu: {
115
+            position: 'relative',
116
+            marginTop: 0,
117
+            right: 'auto',
118
+            padding: '0',
119
+            marginRight: '4px',
120
+            marginBottom: '4px'
121
+        }
122
+    };
123
+};
124
+
140 125
 /**
141 126
  * React {@code Component} for displaying an icon associated with opening the
142 127
  * the {@code VideoMenu}.
@@ -169,6 +154,8 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
169 154
             _overflowDrawer,
170 155
             _showConnectionInfo,
171 156
             _participantDisplayName,
157
+            buttonVisible,
158
+            classes,
172 159
             participantID,
173 160
             popoverVisible
174 161
         } = this.props;
@@ -190,13 +177,14 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
190 177
                 onPopoverOpen = { this._onPopoverOpen }
191 178
                 position = { this.props._menuPosition }
192 179
                 visible = { popoverVisible }>
193
-                {!_overflowDrawer && (
194
-                    <span className = 'popover-trigger remote-video-menu-trigger'>
180
+                {!_overflowDrawer && buttonVisible && (
181
+                    <span
182
+                        className = { classes.triggerButton }
183
+                        role = 'button'>
195 184
                         {!isMobileBrowser() && <Icon
196 185
                             ariaLabel = { this.props.t('dialog.remoteUserControls', { username }) }
197
-                            role = 'button'
198
-                            size = '1.4em'
199
-                            src = { IconMenuThumb }
186
+                            size = { 18 }
187
+                            src = { IconHorizontalPoints }
200 188
                             tabIndex = { 0 }
201 189
                             title = { this.props.t('dialog.remoteUserControls', { username }) } />
202 190
                         }
@@ -245,133 +233,16 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
245 233
      * @returns {ReactElement}
246 234
      */
247 235
     _renderRemoteVideoMenu() {
248
-        const {
249
-            _disableKick,
250
-            _disableRemoteMute,
251
-            _disableGrantModerator,
252
-            _isModerator,
253
-            dispatch,
254
-            initialVolumeValue,
255
-            onVolumeChange,
256
-            _remoteControlState,
257
-            participantID
258
-        } = this.props;
236
+        const { _participant, _remoteControlState, classes } = this.props;
259 237
 
260
-        const actions = [];
261
-        const buttons = [];
262
-        const showVolumeSlider = !isIosMobileBrowser()
263
-              && onVolumeChange
264
-              && typeof initialVolumeValue === 'number'
265
-              && !isNaN(initialVolumeValue);
266
-
267
-        if (_isModerator) {
268
-            if (!_disableRemoteMute) {
269
-                buttons.push(
270
-                    <MuteButton
271
-                        key = 'mute'
272
-                        participantID = { participantID } />
273
-                );
274
-                buttons.push(
275
-                    <MuteEveryoneElseButton
276
-                        key = 'mute-others'
277
-                        participantID = { participantID } />
278
-                );
279
-                buttons.push(
280
-                    <MuteVideoButton
281
-                        key = 'mute-video'
282
-                        participantID = { participantID } />
283
-                );
284
-                buttons.push(
285
-                    <MuteEveryoneElsesVideoButton
286
-                        key = 'mute-others-video'
287
-                        participantID = { participantID } />
288
-                );
289
-            }
290
-
291
-            if (!_disableGrantModerator) {
292
-                buttons.push(
293
-                    <GrantModeratorButton
294
-                        key = 'grant-moderator'
295
-                        participantID = { participantID } />
296
-                );
297
-            }
298
-
299
-            if (!_disableKick) {
300
-                buttons.push(
301
-                    <KickButton
302
-                        key = 'kick'
303
-                        participantID = { participantID } />
304
-                );
305
-            }
306
-        }
307
-
308
-        if (_remoteControlState) {
309
-            let onRemoteControlToggle = null;
310
-
311
-            if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
312
-                onRemoteControlToggle = () => dispatch(stopController(true));
313
-            } else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
314
-                onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID));
315
-            }
316
-
317
-            buttons.push(
318
-                <RemoteControlButton
319
-                    key = 'remote-control'
320
-                    onClick = { onRemoteControlToggle }
321
-                    participantID = { participantID }
322
-                    remoteControlState = { _remoteControlState } />
323
-            );
324
-        }
325
-
326
-        buttons.push(
327
-            <PrivateMessageMenuButton
328
-                key = 'privateMessage'
329
-                participantID = { participantID } />
238
+        return (
239
+            <ParticipantContextMenu
240
+                className = { classes.contextMenu }
241
+                onSelect = { this._onPopoverClose }
242
+                participant = { _participant }
243
+                remoteControlState = { _remoteControlState }
244
+                thumbnailMenu = { true } />
330 245
         );
331
-
332
-        if (isMobileBrowser()) {
333
-            actions.push(
334
-                <ConnectionStatusButton
335
-                    key = 'conn-status'
336
-                    participantId = { participantID } />
337
-            );
338
-        }
339
-
340
-        if (showVolumeSlider) {
341
-            actions.push(
342
-                <VolumeSlider
343
-                    initialValue = { initialVolumeValue }
344
-                    key = 'volume-slider'
345
-                    onChange = { onVolumeChange } />
346
-            );
347
-        }
348
-
349
-        if (buttons.length > 0 || actions.length > 0) {
350
-            return (
351
-                <VideoMenu id = { participantID }>
352
-                    <>
353
-                        { buttons.length > 0
354
-                          && <li onClick = { this.props.hidePopover }>
355
-                              <ul className = 'popupmenu__list'>
356
-                                  { buttons }
357
-                              </ul>
358
-                          </li>
359
-                        }
360
-                    </>
361
-                    <>
362
-                        { actions.length > 0
363
-                         && <li>
364
-                             <ul className = 'popupmenu__list'>
365
-                                 {actions}
366
-                             </ul>
367
-                         </li>
368
-                        }
369
-                    </>
370
-                </VideoMenu>
371
-            );
372
-        }
373
-
374
-        return null;
375 246
     }
376 247
 }
377 248
 
@@ -385,9 +256,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
385 256
  */
386 257
 function _mapStateToProps(state, ownProps) {
387 258
     const { participantID } = ownProps;
388
-    const localParticipant = getLocalParticipant(state);
389
-    const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
390
-    const { disableKick, disableGrantModerator } = remoteVideoMenu;
391 259
     let _remoteControlState = null;
392 260
     const participant = getParticipantById(state, participantID);
393 261
     const _participantDisplayName = participant?.name;
@@ -428,17 +296,14 @@ function _mapStateToProps(state, ownProps) {
428 296
     }
429 297
 
430 298
     return {
431
-        _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
432
-        _disableKick: Boolean(disableKick),
433
-        _disableRemoteMute: Boolean(disableRemoteMute),
434
-        _remoteControlState,
435 299
         _menuPosition,
436 300
         _overflowDrawer: overflowDrawer,
301
+        _participant: participant,
437 302
         _participantDisplayName,
438
-        _disableGrantModerator: Boolean(disableGrantModerator),
303
+        _remoteControlState,
439 304
         _showConnectionInfo: showConnectionInfo
440 305
     };
441 306
 }
442 307
 
443
-export default translate(connect(_mapStateToProps)(RemoteVideoMenuTriggerButton));
444
-/* eslint-enable react/jsx-handler-names */
308
+export default translate(connect(_mapStateToProps)(
309
+    withStyles(styles)(RemoteVideoMenuTriggerButton)));

+ 50
- 0
react/features/video-menu/components/web/SendToRoomButton.js Bestand weergeven

@@ -0,0 +1,50 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+import { useDispatch } from 'react-redux';
6
+
7
+import { createBreakoutRoomsEvent, sendAnalytics } from '../../../analytics';
8
+import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
9
+import { IconRingGroup } from '../../../base/icons';
10
+import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
11
+
12
+type Props = {
13
+
14
+    /**
15
+     * Click handler.
16
+     */
17
+    onClick: ?Function,
18
+
19
+    /**
20
+     * The ID for the participant on which the button will act.
21
+     */
22
+    participantID: string,
23
+
24
+    /**
25
+     * The room to send the participant to.
26
+     */
27
+    room: Object
28
+}
29
+
30
+const SendToRoomButton = ({ onClick, participantID, room }: Props) => {
31
+    const dispatch = useDispatch();
32
+    const { t } = useTranslation();
33
+    const _onClick = useCallback(() => {
34
+        onClick && onClick();
35
+        sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
36
+        dispatch(sendParticipantToRoom(participantID, room.id));
37
+    }, [ participantID, room ]);
38
+
39
+    const roomName = room.name || t('breakoutRooms.mainRoom');
40
+
41
+    return (
42
+        <ContextMenuItem
43
+            accessibilityLabel = { roomName }
44
+            icon = { IconRingGroup }
45
+            onClick = { _onClick }
46
+            text = { roomName } />
47
+    );
48
+};
49
+
50
+export default SendToRoomButton;

+ 0
- 51
react/features/video-menu/components/web/VideoMenu.js Bestand weergeven

@@ -1,51 +0,0 @@
1
-// @flow
2
-
3
-import React from 'react';
4
-
5
-/**
6
- * The type of the React {@code Component} props of {@link VideoMenu}.
7
- */
8
-type Props = {
9
-
10
-    /**
11
-     * The components to place as the body of the {@code VideoMenu}.
12
-     */
13
-    children: React$Node,
14
-
15
-    /**
16
-     * The id attribute to be added to the component's DOM for retrieval when
17
-     * querying the DOM. Not used directly by the component.
18
-     */
19
-    id: string
20
-};
21
-
22
-/**
23
- * Click handler.
24
- *
25
- * @param {SyntheticEvent} event - The click event.
26
- * @returns {void}
27
- */
28
-function onClick(event) {
29
-    // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
30
-    // needs to be stopped.
31
-    event.stopPropagation();
32
-}
33
-
34
-/**
35
- * React {@code Component} responsible for displaying other components as a menu
36
- * for manipulating participant state.
37
- *
38
- * @param {Props} props - The component's props.
39
- * @returns {Component}
40
- */
41
-export default function VideoMenu(props: Props) {
42
-    return (
43
-        <ul
44
-            className = 'popupmenu'
45
-            id = { props.id }
46
-            onClick = { onClick }>
47
-            { props.children }
48
-        </ul>
49
-    );
50
-}
51
-

+ 0
- 111
react/features/video-menu/components/web/VideoMenuButton.js Bestand weergeven

@@ -1,111 +0,0 @@
1
-/* @flow */
2
-
3
-import React, { Component } from 'react';
4
-
5
-import { Icon } from '../../../base/icons';
6
-
7
-/**
8
- * The type of the React {@code Component} props of
9
- * {@link VideoMenuButton}.
10
- */
11
-type Props = {
12
-
13
-    /**
14
-     * Text to display within the component that describes the onClick action.
15
-     */
16
-    buttonText: string,
17
-
18
-    /**
19
-     * Additional CSS classes to add to the component.
20
-     */
21
-    displayClass?: string,
22
-
23
-    /**
24
-     * The icon that will display within the component.
25
-     */
26
-    icon?: Object,
27
-
28
-    /**
29
-     * The id attribute to be added to the component's DOM for retrieval when
30
-     * querying the DOM. Not used directly by the component.
31
-     */
32
-    id: string,
33
-
34
-    /**
35
-     * Callback to invoke when the component is clicked.
36
-     */
37
-    onClick: Function,
38
-};
39
-
40
-/**
41
- * React {@code Component} for displaying an action in {@code VideoMenuButton}.
42
- *
43
- * @augments {Component}
44
- */
45
-export default class VideoMenuButton extends Component<Props> {
46
-    /**
47
-     * Initializes a new {@code RemoteVideoMenuButton} instance.
48
-     *
49
-     * @param {*} props - The read-only properties with which the new instance
50
-     * is to be initialized.
51
-     */
52
-    constructor(props: Props) {
53
-        super(props);
54
-
55
-        // Bind event handler so it is only bound once for every instance.
56
-        this._onKeyPress = this._onKeyPress.bind(this);
57
-    }
58
-
59
-    _onKeyPress: (Object) => void;
60
-
61
-    /**
62
-     * KeyPress handler for accessibility.
63
-     *
64
-     * @param {Object} e - The synthetic event.
65
-     * @returns {void}
66
-     */
67
-    _onKeyPress(e) {
68
-        if (this.props.onClick && (e.key === ' ' || e.key === 'Enter')) {
69
-            e.preventDefault();
70
-            this.props.onClick();
71
-        }
72
-    }
73
-
74
-    /**
75
-     * Implements React's {@link Component#render()}.
76
-     *
77
-     * @inheritdoc
78
-     * @returns {ReactElement}
79
-     */
80
-    render() {
81
-        const {
82
-            buttonText,
83
-            displayClass,
84
-            icon,
85
-            id,
86
-            onClick
87
-        } = this.props;
88
-
89
-        const linkClassName = `popupmenu__link ${displayClass || ''}`;
90
-
91
-        return (
92
-            <li className = 'popupmenu__item'>
93
-                <a
94
-                    aria-label = { buttonText ? buttonText : 'some thing' }
95
-                    className = { linkClassName }
96
-                    id = { id }
97
-                    onClick = { onClick }
98
-                    onKeyPress = { this._onKeyPress }
99
-                    role = 'button'
100
-                    tabIndex = { 0 }>
101
-                    <span className = 'popupmenu__icon'>
102
-                        { icon && <Icon src = { icon } /> }
103
-                    </span>
104
-                    <span className = 'popupmenu__text'>
105
-                        { buttonText }
106
-                    </span>
107
-                </a>
108
-            </li>
109
-        );
110
-    }
111
-}

+ 78
- 21
react/features/video-menu/components/web/VolumeSlider.js Bestand weergeven

@@ -1,5 +1,7 @@
1 1
 /* @flow */
2 2
 
3
+import { withStyles } from '@material-ui/styles';
4
+import clsx from 'clsx';
3 5
 import React, { Component } from 'react';
4 6
 
5 7
 import { translate } from '../../../base/i18n';
@@ -11,6 +13,11 @@ import { VOLUME_SLIDER_SCALE } from '../../constants';
11 13
  */
12 14
 type Props = {
13 15
 
16
+    /**
17
+     * An object containing the CSS classes.
18
+     */
19
+    classes: Object,
20
+
14 21
     /**
15 22
      * The value of the audio slider should display at when the component first
16 23
      * mounts. Changes will be stored in state. The value should be a number
@@ -41,6 +48,43 @@ type State = {
41 48
     volumeLevel: number
42 49
 };
43 50
 
51
+const styles = theme => {
52
+    return {
53
+        container: {
54
+            minHeight: '40px',
55
+            width: '100%',
56
+            boxSizing: 'border-box',
57
+            cursor: 'pointer',
58
+            display: 'flex',
59
+            alignItems: 'center',
60
+            padding: '0 5px',
61
+
62
+            '&:hover': {
63
+                backgroundColor: theme.palette.ui04
64
+            }
65
+        },
66
+
67
+        icon: {
68
+            minWidth: '20px',
69
+            padding: '5px',
70
+            position: 'relative'
71
+        },
72
+
73
+        sliderContainer: {
74
+            position: 'relative',
75
+            width: '100%',
76
+            paddingRight: '5px'
77
+        },
78
+
79
+        slider: {
80
+            position: 'absolute',
81
+            width: '100%',
82
+            top: '50%',
83
+            transform: 'translate(0, -50%)'
84
+        }
85
+    };
86
+};
87
+
44 88
 /**
45 89
  * Implements a React {@link Component} which displays an input slider for
46 90
  * adjusting the local volume of a remote participant.
@@ -65,6 +109,16 @@ class VolumeSlider extends Component<Props, State> {
65 109
         this._onVolumeChange = this._onVolumeChange.bind(this);
66 110
     }
67 111
 
112
+    /**
113
+     * Click handler.
114
+     *
115
+     * @param {MouseEvent} e - Click event.
116
+     * @returns {void}
117
+     */
118
+    _onClick(e) {
119
+        e.stopPropagation();
120
+    }
121
+
68 122
     /**
69 123
      * Implements React's {@link Component#render()}.
70 124
      *
@@ -72,29 +126,32 @@ class VolumeSlider extends Component<Props, State> {
72 126
      * @returns {ReactElement}
73 127
      */
74 128
     render() {
129
+        const { classes } = this.props;
130
+
75 131
         return (
76
-            <li
132
+            <div
77 133
                 aria-label = { this.props.t('volumeSlider') }
78
-                className = 'popupmenu__item'>
79
-                <div className = 'popupmenu__contents'>
80
-                    <span className = 'popupmenu__icon'>
81
-                        <Icon src = { IconVolume } />
82
-                    </span>
83
-                    <div className = 'popupmenu__slider_container'>
84
-                        <input
85
-                            aria-valuemax = { VOLUME_SLIDER_SCALE }
86
-                            aria-valuemin = { 0 }
87
-                            aria-valuenow = { this.state.volumeLevel }
88
-                            className = 'popupmenu__slider'
89
-                            max = { VOLUME_SLIDER_SCALE }
90
-                            min = { 0 }
91
-                            onChange = { this._onVolumeChange }
92
-                            tabIndex = { 0 }
93
-                            type = 'range'
94
-                            value = { this.state.volumeLevel } />
95
-                    </div>
134
+                className = { clsx('popupmenu__contents', classes.container) }
135
+                onClick = { this._onClick }>
136
+                <span className = { classes.icon }>
137
+                    <Icon
138
+                        size = { 22 }
139
+                        src = { IconVolume } />
140
+                </span>
141
+                <div className = { classes.sliderContainer }>
142
+                    <input
143
+                        aria-valuemax = { VOLUME_SLIDER_SCALE }
144
+                        aria-valuemin = { 0 }
145
+                        aria-valuenow = { this.state.volumeLevel }
146
+                        className = { clsx('popupmenu__volume-slider', classes.slider) }
147
+                        max = { VOLUME_SLIDER_SCALE }
148
+                        min = { 0 }
149
+                        onChange = { this._onVolumeChange }
150
+                        tabIndex = { 0 }
151
+                        type = 'range'
152
+                        value = { this.state.volumeLevel } />
96 153
                 </div>
97
-            </li>
154
+            </div>
98 155
         );
99 156
     }
100 157
 
@@ -116,4 +173,4 @@ class VolumeSlider extends Component<Props, State> {
116 173
     }
117 174
 }
118 175
 
119
-export default translate(VolumeSlider);
176
+export default translate(withStyles(styles)(VolumeSlider));

+ 1
- 1
react/features/video-menu/components/web/index.js Bestand weergeven

@@ -1,5 +1,6 @@
1 1
 // @flow
2 2
 
3
+export { default as AskToUnmuteButton } from './AskToUnmuteButton';
3 4
 export { default as ConnectionStatusButton } from './ConnectionStatusButton';
4 5
 export { default as GrantModeratorButton } from './GrantModeratorButton';
5 6
 export { default as GrantModeratorDialog } from './GrantModeratorDialog';
@@ -14,7 +15,6 @@ export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVide
14 15
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
15 16
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
16 17
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
17
-export { default as VideoMenu } from './VideoMenu';
18 18
 export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
19 19
 export { default as LocalVideoMenuTriggerButton } from './LocalVideoMenuTriggerButton';
20 20
 export { default as VolumeSlider } from './VolumeSlider';

Laden…
Annuleren
Opslaan