Browse Source

feat(Filmstrip): Pagination.

master
Hristo Terezov 4 years ago
parent
commit
16cfda3c7a

+ 15
- 53
css/filmstrip/_horizontal_filmstrip.scss View File

@@ -33,18 +33,18 @@
33 33
     }
34 34
 
35 35
     &__videos {
36
-        @extend %align-right;
37 36
         position:relative;
38 37
         padding: 0;
39 38
         /* The filmstrip should not be covered by the left toolbar. */
40 39
         bottom: 0;
41 40
         width:auto;
42
-        overflow: visible !important;
43 41
 
44 42
         &#remoteVideos {
45 43
             border: $thumbnailsBorder solid transparent;
46 44
             transition: bottom 2s;
47 45
             flex-grow: 1;
46
+            display: flex;
47
+            flex-direction: row-reverse;
48 48
             @include minHWAutoFix()
49 49
         }
50 50
 
@@ -60,41 +60,25 @@
60 60
         &.hidden {
61 61
             bottom: calc(-196px - #{$newToolbarSizeWithPadding});
62 62
         }
63
-
64
-        .remote-videos-container {
65
-            display: flex;
66
-        }
67 63
     }
68 64
 
69
-    .remote-videos-container {
70
-        transition: opacity 1s;
71
-    }
65
+    .remote-videos {
66
+        & > div {
67
+            transition: opacity 1s;
68
+            position: absolute;
69
+        }
72 70
 
73
-    &.hide-videos {
74
-        .remote-videos-container {
75
-            opacity: 0;
76
-            pointer-events: none;
71
+        &.is-not-overflowing > div {
72
+            right: 2px;
77 73
         }
78 74
     }
79 75
 
80
-    #filmstripRemoteVideos {
81
-        @include minHWAutoFix();
82
-
83
-        display: flex;
84
-        flex: 1;
85
-        width: auto;
86
-        justify-content: flex-end;
87
-        flex-direction: row;
88
-
89
-        #filmstripRemoteVideosContainer {
90
-            flex-direction: row-reverse;
91
-            /**
92
-             * Add padding as a hack for Firefox not to show scrollbars when
93
-             * unnecessary.
94
-             */
95
-            padding: 1px 0;
96
-            overflow-y: hidden;
97
-            overflow-x: scroll;
76
+    &.hide-videos {
77
+        .remote-videos  {
78
+            & > div {
79
+                opacity: 0;
80
+                pointer-events: none;
81
+            }
98 82
         }
99 83
     }
100 84
 
@@ -103,25 +87,3 @@
103 87
     }
104 88
 }
105 89
 
106
-
107
-/**
108
- * Workarounds for Edge and Firefox not handling scrolling properly with
109
- * flex-direction: row-reverse.
110
- */
111
- @mixin undoRowReverseVideos() {
112
-    .horizontal-filmstrip {
113
-        #remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
114
-            flex-direction: row;
115
-        }
116
-    }
117
-}
118
-
119
-/** Firefox detection hack **/
120
-@-moz-document url-prefix() {
121
-    @include undoRowReverseVideos();
122
-}
123
-
124
-/** Edge detection hack **/
125
-@supports (-ms-ime-align:auto) {
126
-    @include undoRowReverseVideos();
127
-}

+ 39
- 48
css/filmstrip/_tile_view.scss View File

@@ -10,13 +10,11 @@
10 10
         box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
11 11
     }
12 12
 
13
-    #filmstripRemoteVideos {
13
+    .remote-videos {
14 14
         align-items: center;
15 15
         box-sizing: border-box;
16 16
         display: flex;
17 17
         flex-direction: column;
18
-        height: 100%;
19
-        width: 100%;
20 18
     }
21 19
 
22 20
     .filmstrip__videos .videocontainer {
@@ -34,6 +32,9 @@
34 32
          */
35 33
         height: 100% !important;
36 34
         width: 100%;
35
+        display: flex;
36
+        justify-content: center;
37
+        align-items: center;
37 38
     }
38 39
 
39 40
     .filmstrip {
@@ -50,6 +51,10 @@
50 51
             &.shift-right {
51 52
                 margin-left: $sidebarWidth;
52 53
                 width: calc(100% - #{$sidebarWidth});
54
+
55
+                .remote-videos{
56
+                    width: calc(100vw - #{$sidebarWidth});
57
+                }
53 58
             }
54 59
         }
55 60
     }
@@ -62,63 +67,49 @@
62 67
         display: block;
63 68
     }
64 69
 
65
-    #filmstripRemoteVideos {
70
+    .remote-videos {
66 71
         box-sizing: border-box;
67 72
 
68
-        /**
69
-         * Allow vertical scrolling of the thumbnails.
70
-         */
71
-        overflow-x: hidden;
72
-        overflow-y: auto;
73
-    }
74 73
 
75
-    /**
76
-     * The size of the thumbnails should be set with javascript, based on
77
-     * desired column count and window width. The rows are created using flex
78
-     * and allowing the thumbnails to wrap.
79
-     */
80
-    #filmstripRemoteVideosContainer {
81
-        align-content: center;
82
-        align-items: center;
83
-        box-sizing: border-box;
84
-        display: flex;
85
-        flex-wrap: wrap;
86
-        flex-shrink: 0;
87
-        margin-top: auto;
88
-        margin-bottom: auto;
89
-        justify-content: center;
90
-
91
-        .videocontainer {
92
-            border: 0;
74
+        /**
75
+        * The size of the thumbnails should be set with javascript, based on
76
+        * desired column count and window width. The rows are created using flex
77
+        * and allowing the thumbnails to wrap.
78
+        */
79
+        & > div {
80
+            align-content: center;
81
+            align-items: center;
93 82
             box-sizing: border-box;
94
-            display: block;
95
-            margin: 2px;
96
-        }
83
+            display: flex;
84
+            margin-top: auto;
85
+            margin-bottom: auto;
86
+            justify-content: center;
87
+            position: absolute;
97 88
 
98
-        video {
99
-            object-fit: contain;
100
-        }
89
+            .videocontainer {
90
+                border: 0;
91
+                box-sizing: border-box;
92
+                display: block;
93
+                margin: 2px;
94
+            }
101 95
 
102
-        /**
103
-         * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
104
-         */
105
-        @media only screen and (max-width: 500px) {
106 96
             video {
107
-                object-fit: cover;
97
+                object-fit: contain;
108 98
             }
109
-        }
110
-    }
111 99
 
112
-    .has-overflow#filmstripRemoteVideosContainer {
113
-        align-content: baseline;
114
-    }
115
-
116
-    .has-overflow .videocontainer {
117
-        align-self: baseline;
100
+            /**
101
+            * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
102
+            */
103
+            @media only screen and (max-width: 500px) {
104
+                video {
105
+                    object-fit: cover;
106
+                }
107
+            }
108
+        }
118 109
     }
119 110
 }
120 111
 
121
-.shift-right #filmstripRemoteVideosContainer {
112
+.shift-right .remote-videos > div {
122 113
     /**
123 114
      * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
124 115
      * from which we subtract the chat size.

+ 16
- 76
css/filmstrip/_vertical_filmstrip.scss View File

@@ -1,8 +1,10 @@
1 1
 .vertical-filmstrip .filmstrip {
2 2
     &.hide-videos {
3
-        .remote-videos-container {
4
-            opacity: 0;
5
-            pointer-events: none;
3
+        .remote-videos {
4
+            & > div {
5
+                opacity: 0;
6
+                pointer-events: none;
7
+            }
6 8
         }
7 9
     }
8 10
 
@@ -39,10 +41,6 @@
39 41
     right: 0;
40 42
     z-index: $filmstripVideosZ;
41 43
 
42
-    &.reduce-height {
43
-        height: calc(100% - #{$newToolbarSizeWithPadding});
44
-    }
45
-
46 44
     /**
47 45
      * Hide videos by making them slight to the right.
48 46
      */
@@ -98,33 +96,10 @@
98 96
      * filmstrip from overlapping the left edge of the screen.
99 97
      */
100 98
     #filmstripLocalVideo,
101
-    #filmstripRemoteVideos {
99
+    .remote-videos {
102 100
         padding: 0;
103 101
     }
104 102
 
105
-    #filmstripRemoteVideos {
106
-        @include minHWAutoFix();
107
-
108
-        display: flex;
109
-        flex: 1;
110
-        flex-direction: column-reverse;
111
-        height: auto;
112
-        overflow-x: hidden;
113
-        overflow-y: scroll;
114
-
115
-        #filmstripRemoteVideosContainer {
116
-            @include minHWAutoFix();
117
-            flex-direction: column-reverse;
118
-            overflow: visible;
119
-            width: calc(100% - 8px); // 8px for margin + border of the thumbnails
120
-
121
-            .videocontainer {
122
-                height: 0px;
123
-                width: 100%;
124
-            }
125
-        }
126
-    }
127
-
128 103
     #remoteVideos {
129 104
         @include minHWAutoFix();
130 105
 
@@ -132,56 +107,21 @@
132 107
         flex-grow: 1;
133 108
     }
134 109
 
135
-    .remote-videos-container {
136
-        display: flex;
137
-        transition: opacity 1s;
110
+    &.reduce-height {
111
+        height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
138 112
     }
139 113
 
140
-    .hide-scrollbar#filmstripRemoteVideos {
141
-        margin-right: 7px; // Scrollbar size
142
-        &::-webkit-scrollbar {
143
-            display: none;
144
-        }
145
-    }
146
-}
114
+    .remote-videos {
115
+        display: flex;
116
+        transition: height .3s ease-in;
147 117
 
148
-/**
149
- * Workarounds for Edge and Firefox not handling scrolling properly with
150
- * flex-direction: column-reverse. The remove videos in filmstrip should
151
- * start scrolling from the bottom of the filmstrip, but in those browsers the
152
- * scrolling won't happen. Per W3C spec, scrolling should happen from the
153
- * bottom. As such, use css hacks to get around the css issue, with the intent
154
- * being to remove the hacks as the spec is supported.
155
- */
156
-@mixin undoColumnReverseVideos() {
157
-    .vertical-filmstrip {
158
-        #remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
159
-            flex-direction: column;
118
+        & > div {
119
+            position: absolute;
120
+            transition: opacity 1s;
160 121
         }
161
-    }
162
-}
163 122
 
164
-/**
165
- * FF does not include the scroll width when calculating the size of the content. That's why we need to include
166
- * ourselves the width of the scroll so that the remote videos are aligned with the local one.
167
- */
168
-@mixin filmstripSizeWithoutScroll {
169
-    .vertical-filmstrip {
170
-        #remoteVideos #filmstripRemoteVideos {
171
-            #filmstripRemoteVideosContainer {
172
-                width: calc(100% - 15px) // 8 px - margins + border of the thumbnails; 7px - for the scroll
173
-            }
123
+        &.is-not-overflowing > div {
124
+            bottom: 0px;
174 125
         }
175 126
     }
176 127
 }
177
-
178
-/** Firefox detection hack **/
179
-@-moz-document url-prefix() {
180
-    @include undoColumnReverseVideos();
181
-    @include filmstripSizeWithoutScroll();
182
-}
183
-
184
-/** Edge detection hack **/
185
-@supports (-ms-ime-align:auto) {
186
-    @include undoColumnReverseVideos();
187
-}

+ 14
- 0
package-lock.json View File

@@ -11390,6 +11390,11 @@
11390 11390
         }
11391 11391
       }
11392 11392
     },
11393
+    "memoize-one": {
11394
+      "version": "5.1.1",
11395
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
11396
+      "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
11397
+    },
11393 11398
     "memory-fs": {
11394 11399
       "version": "0.4.1",
11395 11400
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -15318,6 +15323,15 @@
15318 15323
         }
15319 15324
       }
15320 15325
     },
15326
+    "react-window": {
15327
+      "version": "1.8.6",
15328
+      "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
15329
+      "integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
15330
+      "requires": {
15331
+        "@babel/runtime": "^7.0.0",
15332
+        "memoize-one": ">=3.1.1 <6"
15333
+      }
15334
+    },
15321 15335
     "react-youtube": {
15322 15336
       "version": "7.13.1",
15323 15337
       "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz",

+ 1
- 0
package.json View File

@@ -94,6 +94,7 @@
94 94
     "react-textarea-autosize": "8.3.0",
95 95
     "react-transition-group": "2.4.0",
96 96
     "react-youtube": "7.13.1",
97
+    "react-window": "1.8.6",
97 98
     "redux": "4.0.4",
98 99
     "redux-thunk": "2.2.0",
99 100
     "rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",

+ 40
- 26
react/features/base/media/components/web/AudioTrack.js View File

@@ -3,6 +3,7 @@
3 3
 import React, { Component } from 'react';
4 4
 
5 5
 import { createAudioPlayErrorEvent, createAudioPlaySuccessEvent, sendAnalytics } from '../../../../analytics';
6
+import { connect } from '../../../redux';
6 7
 import logger from '../../logger';
7 8
 
8 9
 /**
@@ -10,6 +11,16 @@ import logger from '../../logger';
10 11
  */
11 12
 type Props = {
12 13
 
14
+    /**
15
+     * Represents muted property of the underlying audio element.
16
+     */
17
+    _muted: ?Boolean,
18
+
19
+    /**
20
+     * Represents volume property of the underlying audio element.
21
+     */
22
+    _volume: ?number,
23
+
13 24
     /**
14 25
      * The value of the id attribute of the audio element.
15 26
      */
@@ -28,26 +39,15 @@ type Props = {
28 39
     autoPlay: boolean,
29 40
 
30 41
     /**
31
-     * Represents muted property of the underlying audio element.
32
-     */
33
-    muted: ?Boolean,
34
-
35
-    /**
36
-     * Represents volume property of the underlying audio element.
42
+     * The ID of the participant associated with the audio element.
37 43
      */
38
-    volume: ?number,
39
-
40
-    /**
41
-     * A function that will be executed when the reference to the underlying audio element changes in order to report
42
-     * the initial volume value.
43
-     */
44
-    onInitialVolumeSet: Function
44
+    participantId: string
45 45
 };
46 46
 
47 47
 /**
48 48
  * The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
49 49
  */
50
-export default class AudioTrack extends Component<Props> {
50
+class AudioTrack extends Component<Props> {
51 51
     /**
52 52
      * Reference to the HTML audio element, stored until the file is ready.
53 53
      */
@@ -94,14 +94,14 @@ export default class AudioTrack extends Component<Props> {
94 94
         this._attachTrack(this.props.audioTrack);
95 95
 
96 96
         if (this._ref) {
97
-            const { muted, volume } = this.props;
97
+            const { _muted, _volume } = this.props;
98 98
 
99
-            if (typeof volume === 'number') {
100
-                this._ref.volume = volume;
99
+            if (typeof _volume === 'number') {
100
+                this._ref.volume = _volume;
101 101
             }
102 102
 
103
-            if (typeof muted === 'boolean') {
104
-                this._ref.muted = muted;
103
+            if (typeof _muted === 'boolean') {
104
+                this._ref.muted = _muted;
105 105
             }
106 106
         }
107 107
     }
@@ -136,14 +136,14 @@ export default class AudioTrack extends Component<Props> {
136 136
 
137 137
         if (this._ref) {
138 138
             const currentVolume = this._ref.volume;
139
-            const nextVolume = nextProps.volume;
139
+            const nextVolume = nextProps._volume;
140 140
 
141 141
             if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
142 142
                 this._ref.volume = nextVolume;
143 143
             }
144 144
 
145 145
             const currentMuted = this._ref.muted;
146
-            const nextMuted = nextProps.muted;
146
+            const nextMuted = nextProps._muted;
147 147
 
148 148
             if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
149 149
                 this._ref.muted = nextMuted;
@@ -258,10 +258,24 @@ export default class AudioTrack extends Component<Props> {
258 258
      */
259 259
     _setRef(audioElement: ?HTMLAudioElement) {
260 260
         this._ref = audioElement;
261
-        const { onInitialVolumeSet } = this.props;
262
-
263
-        if (this._ref && onInitialVolumeSet) {
264
-            onInitialVolumeSet(this._ref.volume);
265
-        }
266 261
     }
267 262
 }
263
+
264
+/**
265
+ * Maps (parts of) the Redux state to the associated {@code AudioTrack}'s props.
266
+ *
267
+ * @param {Object} state - The Redux state.
268
+ * @param {Object} ownProps - The props passed to the component.
269
+ * @private
270
+ * @returns {Props}
271
+ */
272
+function _mapStateToProps(state, ownProps) {
273
+    const { participantsVolume } = state['features/filmstrip'];
274
+
275
+    return {
276
+        _muted: state['features/base/config'].startSilent,
277
+        _volume: participantsVolume[ownProps.participantId]
278
+    };
279
+}
280
+
281
+export default connect(_mapStateToProps)(AudioTrack);

+ 1
- 0
react/features/base/media/components/web/index.js View File

@@ -1,3 +1,4 @@
1 1
 export { default as Audio } from './Audio';
2
+export { default as AudioTrack } from './AudioTrack';
2 3
 export { default as Video } from './Video';
3 4
 export { default as VideoTrack } from './VideoTrack';

+ 22
- 1
react/features/filmstrip/actionTypes.js View File

@@ -27,7 +27,7 @@ export const SET_FILMSTRIP_VISIBLE = 'SET_FILMSTRIP_VISIBLE';
27 27
  *         gridDimensions: {
28 28
  *             columns: number,
29 29
  *             height: number,
30
- *             visibleRows: number,
30
+ *             minVisibleRows: number,
31 31
  *             width: number
32 32
  *         },
33 33
  *         thumbnailSize: {
@@ -49,3 +49,24 @@ export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS';
49 49
  * }
50 50
  */
51 51
 export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
52
+
53
+/**
54
+ * The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
55
+ *
56
+ * {
57
+ *     type: SET_VERTICAL_VIEW_DIMENSIONS,
58
+ *     dimensions: Object
59
+ * }
60
+ */
61
+export const SET_VERTICAL_VIEW_DIMENSIONS = 'SET_VERTICAL_VIEW_DIMENSIONS';
62
+
63
+/**
64
+ * The type of (redux) action which sets the volume for a thumnail's audio.
65
+ *
66
+ * {
67
+ *     type: SET_VOLUME,
68
+ *     participantId: string,
69
+ *     volume: number
70
+ * }
71
+ */
72
+export const SET_VOLUME = 'SET_VOLUME';

+ 115
- 40
react/features/filmstrip/actions.web.js View File

@@ -1,64 +1,120 @@
1 1
 // @flow
2
+import type { Dispatch } from 'redux';
2 3
 
3 4
 import { pinParticipant } from '../base/participants';
4
-import { toState } from '../base/redux';
5 5
 
6
-import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
7
-import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions';
8
-
9
-/**
10
- * The size of the side margins for the entire tile view area.
11
- */
12
-const TILE_VIEW_SIDE_MARGINS = 20;
6
+import {
7
+    SET_HORIZONTAL_VIEW_DIMENSIONS,
8
+    SET_TILE_VIEW_DIMENSIONS,
9
+    SET_VERTICAL_VIEW_DIMENSIONS,
10
+    SET_VOLUME
11
+} from './actionTypes';
12
+import {
13
+    HORIZONTAL_FILMSTRIP_MARGIN,
14
+    SCROLL_SIZE,
15
+    STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
16
+    STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
17
+    TILE_HORIZONTAL_MARGIN,
18
+    TILE_VERTICAL_MARGIN,
19
+    VERTICAL_FILMSTRIP_VERTICAL_MARGIN
20
+} from './constants';
21
+import {
22
+    calculateThumbnailSizeForHorizontalView,
23
+    calculateThumbnailSizeForTileView,
24
+    calculateThumbnailSizeForVerticalView
25
+} from './functions';
13 26
 
14 27
 /**
15 28
  * Sets the dimensions of the tile view grid.
16 29
  *
17 30
  * @param {Object} dimensions - Whether the filmstrip is visible.
18
- * @param {Object} windowSize - The size of the window.
19 31
  * @param {Object | Function} stateful - An object or function that can be
20 32
  * resolved to Redux state using the {@code toState} function.
21
- * @returns {{
22
- *     type: SET_TILE_VIEW_DIMENSIONS,
23
- *     dimensions: Object
24
- * }}
33
+ * @returns {Function}
25 34
  */
26
-export function setTileViewDimensions(dimensions: Object, windowSize: Object, stateful: Object | Function) {
27
-    const state = toState(stateful);
28
-    const { clientWidth, clientHeight } = windowSize;
29
-    const { disableResponsiveTiles } = state['features/base/config'];
35
+export function setTileViewDimensions(dimensions: Object) {
36
+    return (dispatch: Dispatch<any>, getState: Function) => {
37
+        const state = getState();
38
+        const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
39
+        const { disableResponsiveTiles } = state['features/base/config'];
40
+        const {
41
+            height,
42
+            width
43
+        } = calculateThumbnailSizeForTileView({
44
+            ...dimensions,
45
+            clientWidth,
46
+            clientHeight,
47
+            disableResponsiveTiles
48
+        });
49
+        const { columns, rows } = dimensions;
50
+        const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
51
+        const hasScroll = clientHeight < thumbnailsTotalHeight;
52
+        const filmstripWidth = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
53
+        const filmstripHeight = Math.min(clientHeight, thumbnailsTotalHeight);
30 54
 
31
-    const thumbnailSize = calculateThumbnailSizeForTileView({
32
-        ...dimensions,
33
-        clientWidth,
34
-        clientHeight,
35
-        disableResponsiveTiles
36
-    });
37
-    const filmstripWidth = dimensions.columns * (TILE_VIEW_SIDE_MARGINS + thumbnailSize.width);
55
+        dispatch({
56
+            type: SET_TILE_VIEW_DIMENSIONS,
57
+            dimensions: {
58
+                gridDimensions: dimensions,
59
+                thumbnailSize: {
60
+                    height,
61
+                    width
62
+                },
63
+                filmstripHeight,
64
+                filmstripWidth
65
+            }
66
+        });
67
+    };
68
+}
38 69
 
39
-    return {
40
-        type: SET_TILE_VIEW_DIMENSIONS,
41
-        dimensions: {
42
-            gridDimensions: dimensions,
43
-            thumbnailSize,
44
-            filmstripWidth
45
-        }
70
+/**
71
+ * Sets the dimensions of the thumbnails in vertical view.
72
+ *
73
+ * @returns {Function}
74
+ */
75
+export function setVerticalViewDimensions() {
76
+    return (dispatch: Dispatch<any>, getState: Function) => {
77
+        const state = getState();
78
+        const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
79
+        const thumbnails = calculateThumbnailSizeForVerticalView(clientWidth);
80
+
81
+        dispatch({
82
+            type: SET_VERTICAL_VIEW_DIMENSIONS,
83
+            dimensions: {
84
+                ...thumbnails,
85
+                remoteVideosContainer: {
86
+                    width: thumbnails?.local?.width
87
+                        + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER + SCROLL_SIZE,
88
+                    height: clientHeight - thumbnails?.local?.height - VERTICAL_FILMSTRIP_VERTICAL_MARGIN
89
+                }
90
+            }
91
+
92
+        });
46 93
     };
47 94
 }
48 95
 
49 96
 /**
50 97
  * Sets the dimensions of the thumbnails in horizontal view.
51 98
  *
52
- * @param {number} clientHeight - The height of the window.
53
- * @returns {{
54
- *     type: SET_HORIZONTAL_VIEW_DIMENSIONS,
55
- *     dimensions: Object
56
- * }}
99
+ * @returns {Function}
57 100
  */
58
-export function setHorizontalViewDimensions(clientHeight: number = 0) {
59
-    return {
60
-        type: SET_HORIZONTAL_VIEW_DIMENSIONS,
61
-        dimensions: calculateThumbnailSizeForHorizontalView(clientHeight)
101
+export function setHorizontalViewDimensions() {
102
+    return (dispatch: Dispatch<any>, getState: Function) => {
103
+        const state = getState();
104
+        const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
105
+        const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
106
+
107
+        dispatch({
108
+            type: SET_HORIZONTAL_VIEW_DIMENSIONS,
109
+            dimensions: {
110
+                ...thumbnails,
111
+                remoteVideosContainer: {
112
+                    width: clientWidth - thumbnails?.local?.width - HORIZONTAL_FILMSTRIP_MARGIN,
113
+                    height: thumbnails?.local?.height
114
+                        + TILE_VERTICAL_MARGIN + STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER + SCROLL_SIZE
115
+                }
116
+            }
117
+        });
62 118
     };
63 119
 }
64 120
 
@@ -78,4 +134,23 @@ export function clickOnVideo(n: number) {
78 134
     };
79 135
 }
80 136
 
137
+/**
138
+ * Sets the volume for a thumnail's audio.
139
+ *
140
+ * @param {string} participantId - The participant ID asociated with the audio.
141
+ * @param {string} volume - The volume level.
142
+ * @returns {{
143
+ *     type: SET_VOLUME,
144
+ *     participantId: string,
145
+ *     volume: number
146
+ * }}
147
+ */
148
+export function setVolume(participantId: string, volume: number) {
149
+    return {
150
+        type: SET_VOLUME,
151
+        participantId,
152
+        volume
153
+    };
154
+}
155
+
81 156
 export * from './actions.native';

+ 65
- 0
react/features/filmstrip/components/web/AudioTracksContainer.js View File

@@ -0,0 +1,65 @@
1
+/* @flow */
2
+import React from 'react';
3
+
4
+import { AudioTrack, MEDIA_TYPE } from '../../../base/media';
5
+import { connect } from '../../../base/redux';
6
+
7
+/**
8
+ * The type of the React {@code Component} props of {@link AudioTracksContainer}.
9
+ */
10
+type Props = {
11
+
12
+    /**
13
+     * All media tracks stored in redux.
14
+     */
15
+    _tracks: Array<Object>
16
+};
17
+
18
+/**
19
+ * A container for the remote tracks audio elements.
20
+ *
21
+ * @param {Props} props - The props of the component.
22
+ * @returns {Array<ReactElement>}
23
+ */
24
+function AudioTracksContainer(props: Props) {
25
+    const { _tracks } = props;
26
+    const remoteAudioTracks = _tracks.filter(t => !t.local && t.mediaType === MEDIA_TYPE.AUDIO);
27
+
28
+    return (
29
+        <div>
30
+            {
31
+                remoteAudioTracks.map(t => {
32
+                    const { jitsiTrack, participantId } = t;
33
+                    const audioTrackId = jitsiTrack && jitsiTrack.getId();
34
+                    const id = `remoteAudio_${audioTrackId || ''}`;
35
+
36
+                    return (
37
+                        <AudioTrack
38
+                            audioTrack = { t }
39
+                            id = { id }
40
+                            key = { id }
41
+                            participantId = { participantId } />);
42
+                })
43
+            }
44
+        </div>);
45
+}
46
+
47
+/**
48
+ * Maps (parts of) the Redux state to the associated {@code AudioTracksContainer}'s props.
49
+ *
50
+ * @param {Object} state - The Redux state.
51
+ * @private
52
+ * @returns {Props}
53
+ */
54
+function _mapStateToProps(state) {
55
+    // NOTE: The disadvantage of this approach is that the component will re-render on any track change.
56
+    // One way to solve the problem would be to pass only the participant ID to the AudioTrack component and
57
+    // find the corresponding track inside the AudioTrack's mapStateToProps. But currently this will be very
58
+    // inefficient because features/base/tracks is an array and in order to find a track by participant ID
59
+    // we need to go trough the array. Introducing a map participantID -> track could be beneficial in this case.
60
+    return {
61
+        _tracks: state['features/base/tracks']
62
+    };
63
+}
64
+
65
+export default connect(_mapStateToProps)(AudioTracksContainer);

+ 204
- 75
react/features/filmstrip/components/web/Filmstrip.js View File

@@ -1,6 +1,7 @@
1 1
 /* @flow */
2 2
 
3
-import React, { Component } from 'react';
3
+import React, { PureComponent } from 'react';
4
+import { FixedSizeList, FixedSizeGrid } from 'react-window';
4 5
 import type { Dispatch } from 'redux';
5 6
 
6 7
 import {
@@ -11,15 +12,17 @@ import {
11 12
 import { getToolbarButtons } from '../../../base/config';
12 13
 import { translate } from '../../../base/i18n';
13 14
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
14
-import { getLocalParticipant } from '../../../base/participants';
15 15
 import { connect } from '../../../base/redux';
16 16
 import { showToolbox } from '../../../toolbox/actions.web';
17 17
 import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
18 18
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
19 19
 import { setFilmstripVisible } from '../../actions';
20
+import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants';
20 21
 import { shouldRemoteVideosBeVisible } from '../../functions';
21 22
 
23
+import AudioTracksContainer from './AudioTracksContainer';
22 24
 import Thumbnail from './Thumbnail';
25
+import ThumbnailWrapper from './ThumbnailWrapper';
23 26
 
24 27
 declare var APP: Object;
25 28
 declare var interfaceConfig: Object;
@@ -50,14 +53,9 @@ type Props = {
50 53
     _filmstripWidth: number,
51 54
 
52 55
     /**
53
-     * Whether the filmstrip scrollbar should be hidden or not.
56
+     * The height of the filmstrip.
54 57
      */
55
-    _hideScrollbar: boolean,
56
-
57
-    /**
58
-     * Whether the filmstrip toolbar should be hidden or not.
59
-     */
60
-    _hideToolbar: boolean,
58
+    _filmstripHeight: number,
61 59
 
62 60
     /**
63 61
      * Whether the filmstrip button is enabled.
@@ -67,13 +65,29 @@ type Props = {
67 65
     /**
68 66
      * The participants in the call.
69 67
      */
70
-    _participants: Array<Object>,
68
+    _remoteParticipants: Array<Object>,
69
+
70
+
71
+    /**
72
+     * The length of the remote participants array.
73
+     */
74
+    _remoteParticipantsLength: number,
71 75
 
72 76
     /**
73 77
      * The number of rows in tile view.
74 78
      */
75 79
     _rows: number,
76 80
 
81
+    /**
82
+     * The height of the thumbnail.
83
+     */
84
+    _thumbnailHeight: number,
85
+
86
+    /**
87
+     * The width of the thumbnail.
88
+     */
89
+    _thumbnailWidth: number,
90
+
77 91
     /**
78 92
      * Additional CSS class names to add to the container of all the thumbnails.
79 93
      */
@@ -106,7 +120,7 @@ type Props = {
106 120
  *
107 121
  * @extends Component
108 122
  */
109
-class Filmstrip extends Component <Props> {
123
+class Filmstrip extends PureComponent <Props> {
110 124
 
111 125
     /**
112 126
      * Initializes a new {@code Filmstrip} instance.
@@ -121,6 +135,8 @@ class Filmstrip extends Component <Props> {
121 135
         this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
122 136
         this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
123 137
         this._onTabIn = this._onTabIn.bind(this);
138
+        this._gridItemKey = this._gridItemKey.bind(this);
139
+        this._listItemKey = this._listItemKey.bind(this);
124 140
     }
125 141
 
126 142
     /**
@@ -154,11 +170,7 @@ class Filmstrip extends Component <Props> {
154 170
      */
155 171
     render() {
156 172
         const filmstripStyle = { };
157
-        const filmstripRemoteVideosContainerStyle = {};
158
-        let remoteVideoContainerClassName = 'remote-videos-container';
159
-        const { _currentLayout, _participants } = this.props;
160
-        const remoteParticipants = _participants.filter(p => !p.local);
161
-        const localParticipant = getLocalParticipant(_participants);
173
+        const { _currentLayout } = this.props;
162 174
         const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
163 175
 
164 176
         switch (_currentLayout) {
@@ -167,28 +179,11 @@ class Filmstrip extends Component <Props> {
167 179
             // Also adding 7px for the scrollbar.
168 180
             filmstripStyle.maxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 25;
169 181
             break;
170
-        case LAYOUTS.TILE_VIEW: {
171
-            // The size of the side margins for each tile as set in CSS.
172
-            const { _columns, _rows, _filmstripWidth } = this.props;
173
-
174
-            if (_rows > _columns) {
175
-                remoteVideoContainerClassName += ' has-overflow';
176
-            }
177
-
178
-            filmstripRemoteVideosContainerStyle.width = _filmstripWidth;
179
-            break;
180
-        }
181
-        }
182
-
183
-        let remoteVideosWrapperClassName = 'filmstrip__videos';
184
-
185
-        if (this.props._hideScrollbar) {
186
-            remoteVideosWrapperClassName += ' hide-scrollbar';
187 182
         }
188 183
 
189 184
         let toolbar = null;
190 185
 
191
-        if (!this.props._hideToolbar && this.props._isFilmstripButtonEnabled) {
186
+        if (this.props._isFilmstripButtonEnabled) {
192 187
             toolbar = this._renderToggleButton();
193 188
         }
194 189
 
@@ -206,41 +201,15 @@ class Filmstrip extends Component <Props> {
206 201
                         <div id = 'filmstripLocalVideoThumbnail'>
207 202
                             {
208 203
                                 !tileViewActive && <Thumbnail
209
-                                    key = 'local'
210
-                                    participantID = { localParticipant.id } />
204
+                                    key = 'local' />
211 205
                             }
212 206
                         </div>
213 207
                     </div>
214
-                    <div
215
-                        className = { remoteVideosWrapperClassName }
216
-                        id = 'filmstripRemoteVideos'>
217
-                        {/*
218
-                          * XXX This extra video container is needed for
219
-                          * scrolling thumbnails in Firefox; otherwise, the flex
220
-                          * thumbnails resize instead of causing overflow.
221
-                          */}
222
-                        <div
223
-                            className = { remoteVideoContainerClassName }
224
-                            id = 'filmstripRemoteVideosContainer'
225
-                            style = { filmstripRemoteVideosContainerStyle }>
226
-                            {
227
-                                remoteParticipants.map(
228
-                                    p => (
229
-                                        <Thumbnail
230
-                                            key = { `remote_${p.id}` }
231
-                                            participantID = { p.id } />
232
-                                    ))
233
-                            }
234
-                            <div id = 'localVideoTileViewContainer'>
235
-                                {
236
-                                    tileViewActive && <Thumbnail
237
-                                        key = 'local'
238
-                                        participantID = { localParticipant.id } />
239
-                                }
240
-                            </div>
241
-                        </div>
242
-                    </div>
208
+                    {
209
+                        this._renderRemoteParticipants()
210
+                    }
243 211
                 </div>
212
+                <AudioTracksContainer />
244 213
             </div>
245 214
         );
246 215
     }
@@ -258,6 +227,135 @@ class Filmstrip extends Component <Props> {
258 227
         }
259 228
     }
260 229
 
230
+    _listItemKey: number => string;
231
+
232
+    /**
233
+     * The key to be used for every ThumbnailWrapper element in stage view.
234
+     *
235
+     * @param {number} index - The index of the ThumbnailWrapper instance.
236
+     * @returns {string} - The key.
237
+     */
238
+    _listItemKey(index) {
239
+        const { _remoteParticipants, _remoteParticipantsLength } = this.props;
240
+
241
+        if (typeof index !== 'number' || _remoteParticipantsLength <= index) {
242
+            return `empty-${index}`;
243
+        }
244
+
245
+        return _remoteParticipants[_remoteParticipantsLength - index - 1];
246
+    }
247
+
248
+    _gridItemKey: Object => string;
249
+
250
+    /**
251
+     * The key to be used for every ThumbnailWrapper element in tile views.
252
+     *
253
+     * @param {Object} data - An object with the indexes identifying the ThumbnailWrapper instance.
254
+     * @returns {string} - The key.
255
+     */
256
+    _gridItemKey({ columnIndex, rowIndex }) {
257
+        const { _columns, _remoteParticipants, _remoteParticipantsLength } = this.props;
258
+        const index = (rowIndex * _columns) + columnIndex;
259
+
260
+        if (index > _remoteParticipantsLength) {
261
+            return `empty-${index}`;
262
+        }
263
+
264
+        if (index === _remoteParticipantsLength) {
265
+            return 'local';
266
+        }
267
+
268
+        return _remoteParticipants[index];
269
+    }
270
+
271
+    /**
272
+     * Renders the thumbnails for remote participants.
273
+     *
274
+     * @returns {ReactElement}
275
+     */
276
+    _renderRemoteParticipants() {
277
+        const {
278
+            _columns,
279
+            _currentLayout,
280
+            _filmstripHeight,
281
+            _filmstripWidth,
282
+            _remoteParticipantsLength,
283
+            _rows,
284
+            _thumbnailHeight,
285
+            _thumbnailWidth
286
+        } = this.props;
287
+
288
+        if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
289
+            || isNaN(_thumbnailHeight) || !_filmstripHeight || isNaN(_filmstripHeight) || !_filmstripWidth
290
+            || isNaN(_filmstripWidth)) {
291
+            return null;
292
+        }
293
+
294
+        if (_currentLayout === LAYOUTS.TILE_VIEW) {
295
+            return (
296
+                <FixedSizeGrid
297
+                    className = 'filmstrip__videos remote-videos'
298
+                    columnCount = { _columns }
299
+                    columnWidth = { _thumbnailWidth + TILE_HORIZONTAL_MARGIN }
300
+                    height = { _filmstripHeight }
301
+                    initialScrollLeft = { 0 }
302
+                    initialScrollTop = { 0 }
303
+                    itemKey = { this._gridItemKey }
304
+                    rowCount = { _rows }
305
+                    rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
306
+                    width = { _filmstripWidth }>
307
+                    {
308
+                        ThumbnailWrapper
309
+                    }
310
+                </FixedSizeGrid>
311
+            );
312
+        }
313
+
314
+
315
+        const props = {
316
+            itemCount: _remoteParticipantsLength,
317
+            className: 'filmstrip__videos remote-videos',
318
+            height: _filmstripHeight,
319
+            itemKey: this._listItemKey,
320
+            itemSize: 0,
321
+            width: _filmstripWidth,
322
+            style: {
323
+                willChange: 'auto'
324
+            }
325
+        };
326
+
327
+        if (_currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
328
+            const itemSize = _thumbnailWidth + TILE_HORIZONTAL_MARGIN;
329
+            const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripWidth;
330
+
331
+            props.itemSize = itemSize;
332
+
333
+            // $FlowFixMe
334
+            props.layout = 'horizontal';
335
+            if (isNotOverflowing) {
336
+                props.className += ' is-not-overflowing';
337
+            }
338
+
339
+        } else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
340
+            const itemSize = _thumbnailHeight + TILE_VERTICAL_MARGIN;
341
+            const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripHeight;
342
+
343
+            if (isNotOverflowing) {
344
+                props.className += ' is-not-overflowing';
345
+            }
346
+
347
+            props.itemSize = itemSize;
348
+        }
349
+
350
+        return (
351
+            <FixedSizeList { ...props }>
352
+                {
353
+                    ThumbnailWrapper
354
+                }
355
+            </FixedSizeList>
356
+        );
357
+    }
358
+
261 359
     /**
262 360
      * Dispatches an action to change the visibility of the filmstrip.
263 361
      *
@@ -344,29 +442,60 @@ class Filmstrip extends Component <Props> {
344 442
  * @returns {Props}
345 443
  */
346 444
 function _mapStateToProps(state) {
347
-    const { iAmSipGateway } = state['features/base/config'];
348 445
     const toolbarButtons = getToolbarButtons(state);
349
-    const { visible } = state['features/filmstrip'];
350
-    const reduceHeight
351
-        = state['features/toolbox'].visible && toolbarButtons.length;
446
+    const { visible, remoteParticipants } = state['features/filmstrip'];
447
+    const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
352 448
     const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
353 449
     const { isOpen: shiftRight } = state['features/chat'];
354 450
     const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
355 451
         reduceHeight ? 'reduce-height' : ''
356 452
     } ${shiftRight ? 'shift-right' : ''}`.trim();
357 453
     const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
358
-    const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
454
+    const {
455
+        gridDimensions = {},
456
+        filmstripHeight,
457
+        filmstripWidth,
458
+        thumbnailSize: tileViewThumbnailSize
459
+    } = state['features/filmstrip'].tileViewDimensions;
460
+    const _currentLayout = getCurrentLayout(state);
461
+    let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
462
+
463
+    switch (_currentLayout) {
464
+    case LAYOUTS.TILE_VIEW:
465
+        _thumbnailSize = tileViewThumbnailSize;
466
+        remoteFilmstripHeight = filmstripHeight;
467
+        remoteFilmstripWidth = filmstripWidth;
468
+        break;
469
+    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
470
+        const { remote, remoteVideosContainer } = state['features/filmstrip'].verticalViewDimensions;
471
+
472
+        _thumbnailSize = remote;
473
+        remoteFilmstripHeight = remoteVideosContainer?.height - (reduceHeight ? TOOLBAR_HEIGHT : 0);
474
+        remoteFilmstripWidth = remoteVideosContainer?.width;
475
+        break;
476
+    }
477
+    case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
478
+        const { remote, remoteVideosContainer } = state['features/filmstrip'].horizontalViewDimensions;
479
+
480
+        _thumbnailSize = remote;
481
+        remoteFilmstripHeight = remoteVideosContainer?.height;
482
+        remoteFilmstripWidth = remoteVideosContainer?.width;
483
+        break;
484
+    }
485
+    }
359 486
 
360 487
     return {
361 488
         _className: className,
362 489
         _columns: gridDimensions.columns,
363
-        _currentLayout: getCurrentLayout(state),
364
-        _filmstripWidth: filmstripWidth,
365
-        _hideScrollbar: Boolean(iAmSipGateway),
366
-        _hideToolbar: Boolean(iAmSipGateway),
490
+        _currentLayout,
491
+        _filmstripHeight: remoteFilmstripHeight,
492
+        _filmstripWidth: remoteFilmstripWidth,
367 493
         _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
368
-        _participants: state['features/base/participants'],
494
+        _remoteParticipantsLength: remoteParticipants.length,
495
+        _remoteParticipants: remoteParticipants,
369 496
         _rows: gridDimensions.rows,
497
+        _thumbnailWidth: _thumbnailSize?.width,
498
+        _thumbnailHeight: _thumbnailSize?.height,
370 499
         _videosClassName: videosClassName,
371 500
         _visible: visible,
372 501
         _isToolboxVisible: isToolboxVisible(state)

+ 62
- 84
react/features/filmstrip/components/web/Thumbnail.js View File

@@ -7,7 +7,6 @@ import { AudioLevelIndicator } from '../../../audio-level-indicator';
7 7
 import { Avatar } from '../../../base/avatar';
8 8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
9 9
 import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
10
-import AudioTrack from '../../../base/media/components/web/AudioTrack';
11 10
 import {
12 11
     getLocalParticipant,
13 12
     getParticipantById,
@@ -28,6 +27,7 @@ import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from
28 27
 import { PresenceLabel } from '../../../presence-status';
29 28
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
30 29
 import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
30
+import { setVolume } from '../../actions.web';
31 31
 import {
32 32
     DISPLAY_MODE_TO_CLASS_NAME,
33 33
     DISPLAY_MODE_TO_STRING,
@@ -65,12 +65,7 @@ export type State = {|
65 65
     /**
66 66
      * Indicates whether the thumbnail is hovered or not.
67 67
      */
68
-    isHovered: boolean,
69
-
70
-    /**
71
-     * The current volume setting for the Thumbnail.
72
-     */
73
-    volume: ?number
68
+    isHovered: boolean
74 69
 |};
75 70
 
76 71
 /**
@@ -179,9 +174,9 @@ export type Props = {|
179 174
     _participant: Object,
180 175
 
181 176
     /**
182
-     * The number of participants in the call.
177
+     * True if there are more than 2 participants in the call.
183 178
      */
184
-    _participantCount: number,
179
+     _participantCountMoreThan2: boolean,
185 180
 
186 181
     /**
187 182
      * Indicates whether the "start silent" mode is enabled.
@@ -193,6 +188,11 @@ export type Props = {|
193 188
      */
194 189
     _videoTrack: ?Object,
195 190
 
191
+    /**
192
+     * The volume level for the thumbnail.
193
+     */
194
+    _volume?: ?number,
195
+
196 196
     /**
197 197
      * The width of the thumbnail.
198 198
      */
@@ -203,10 +203,20 @@ export type Props = {|
203 203
      */
204 204
     dispatch: Function,
205 205
 
206
+    /**
207
+     * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
208
+     */
209
+    horizontalOffset: number,
210
+
206 211
     /**
207 212
      * The ID of the participant related to the thumbnail.
208 213
      */
209
-    participantID: ?string
214
+    participantID: ?string,
215
+
216
+    /**
217
+     * Styles that will be set to the Thumbnail's main span element.
218
+     */
219
+    style?: ?Object
210 220
 |};
211 221
 
212 222
 /**
@@ -240,7 +250,6 @@ class Thumbnail extends Component<Props, State> {
240 250
             audioLevel: 0,
241 251
             canPlayEventReceived: false,
242 252
             isHovered: false,
243
-            volume: undefined,
244 253
             displayMode: DISPLAY_VIDEO
245 254
         };
246 255
 
@@ -253,7 +262,6 @@ class Thumbnail extends Component<Props, State> {
253 262
         this._onCanPlay = this._onCanPlay.bind(this);
254 263
         this._onClick = this._onClick.bind(this);
255 264
         this._onVolumeChange = this._onVolumeChange.bind(this);
256
-        this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
257 265
         this._onMouseEnter = this._onMouseEnter.bind(this);
258 266
         this._onMouseLeave = this._onMouseLeave.bind(this);
259 267
         this._onTestingEvent = this._onTestingEvent.bind(this);
@@ -457,7 +465,7 @@ class Thumbnail extends Component<Props, State> {
457 465
      * @returns {Object} - The styles for the thumbnail.
458 466
      */
459 467
     _getStyles(): Object {
460
-        const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
468
+        const { _height, _isHidden, _width, style, horizontalOffset } = this.props;
461 469
         let styles: {
462 470
             thumbnail: Object,
463 471
             avatar: Object
@@ -466,39 +474,28 @@ class Thumbnail extends Component<Props, State> {
466 474
             avatar: {}
467 475
         };
468 476
 
469
-        switch (_currentLayout) {
470
-        case LAYOUTS.TILE_VIEW:
471
-        case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
472
-            const avatarSize = _height / 2;
473
-
474
-            styles = {
475
-                thumbnail: {
476
-                    height: `${_height}px`,
477
-                    minHeight: `${_height}px`,
478
-                    minWidth: `${_width}px`,
479
-                    width: `${_width}px`
480
-                },
481
-                avatar: {
482
-                    height: `${avatarSize}px`,
483
-                    width: `${avatarSize}px`
484
-                }
485
-            };
486
-            break;
487
-        }
488
-        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
489
-            styles = {
490
-                thumbnail: {
491
-                    paddingTop: `${_heightToWidthPercent}%`
492
-                },
493
-                avatar: {
494
-                    height: '50%',
495
-                    width: `${_heightToWidthPercent / 2}%`
496
-                }
497
-            };
498
-            break;
499
-        }
477
+        const avatarSize = _height / 2;
478
+        let { left } = style || {};
479
+
480
+        if (typeof left === 'number' && horizontalOffset) {
481
+            left += horizontalOffset;
500 482
         }
501 483
 
484
+        styles = {
485
+            thumbnail: {
486
+                ...style,
487
+                left,
488
+                height: `${_height}px`,
489
+                minHeight: `${_height}px`,
490
+                minWidth: `${_width}px`,
491
+                width: `${_width}px`
492
+            },
493
+            avatar: {
494
+                height: `${avatarSize}px`,
495
+                width: `${avatarSize}px`
496
+            }
497
+        };
498
+
502 499
         if (_isHidden) {
503 500
             styles.thumbnail.display = 'none';
504 501
         }
@@ -584,7 +581,7 @@ class Thumbnail extends Component<Props, State> {
584 581
             _isDominantSpeakerDisabled,
585 582
             _indicatorIconSize: iconSize,
586 583
             _participant,
587
-            _participantCount
584
+            _participantCountMoreThan2
588 585
         } = this.props;
589 586
         const { isHovered } = this.state;
590 587
         const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
@@ -621,7 +618,7 @@ class Thumbnail extends Component<Props, State> {
621 618
                     iconSize = { iconSize }
622 619
                     participantId = { id }
623 620
                     tooltipPosition = { tooltipPosition } />
624
-                { showDominantSpeaker && _participantCount > 2
621
+                { showDominantSpeaker && _participantCountMoreThan2
625 622
                     && <DominantSpeakerIndicator
626 623
                         iconSize = { iconSize }
627 624
                         tooltipPosition = { tooltipPosition } />
@@ -793,21 +790,19 @@ class Thumbnail extends Component<Props, State> {
793 790
      */
794 791
     _renderRemoteParticipant() {
795 792
         const {
796
-            _audioTrack,
797 793
             _isTestModeEnabled,
798 794
             _participant,
799 795
             _startSilent,
800
-            _videoTrack
796
+            _videoTrack,
797
+            _volume = 1
801 798
         } = this.props;
802 799
         const { id } = _participant;
803
-        const { audioLevel, canPlayEventReceived, volume } = this.state;
800
+        const { audioLevel, canPlayEventReceived } = this.state;
804 801
         const styles = this._getStyles();
805 802
         const containerClassName = this._getContainerClassName();
806 803
 
807 804
         // hide volume when in silent mode
808 805
         const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
809
-        const jitsiAudioTrack = _audioTrack?.jitsiTrack;
810
-        const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
811 806
         const jitsiVideoTrack = _videoTrack?.jitsiTrack;
812 807
         const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
813 808
         const videoEventListeners = {};
@@ -840,14 +835,6 @@ class Thumbnail extends Component<Props, State> {
840 835
                         style = { videoElementStyle }
841 836
                         videoTrack = { _videoTrack } />
842 837
                 }
843
-                {
844
-                    _audioTrack && <AudioTrack
845
-                        audioTrack = { _audioTrack }
846
-                        id = { `remoteAudio_${audioTrackId || ''}` }
847
-                        muted = { _startSilent }
848
-                        onInitialVolumeSet = { this._onInitialVolumeSet }
849
-                        volume = { volume } />
850
-                }
851 838
                 <div className = 'videocontainer__background' />
852 839
                 <div className = 'videocontainer__toptoolbar'>
853 840
                     { this._renderTopIndicators() }
@@ -872,7 +859,7 @@ class Thumbnail extends Component<Props, State> {
872 859
                 </span>
873 860
                 <span className = 'remotevideomenu'>
874 861
                     <RemoteVideoMenuTriggerButton
875
-                        initialVolumeValue = { volume }
862
+                        initialVolumeValue = { _volume }
876 863
                         onVolumeChange = { onVolumeChange }
877 864
                         participantID = { id } />
878 865
                 </span>
@@ -880,20 +867,6 @@ class Thumbnail extends Component<Props, State> {
880 867
         );
881 868
     }
882 869
 
883
-    _onInitialVolumeSet: Object => void;
884
-
885
-    /**
886
-     * A handler for the initial volume value of the audio element.
887
-     *
888
-     * @param {number} volume - Properties of the audio element.
889
-     * @returns {void}
890
-     */
891
-    _onInitialVolumeSet(volume) {
892
-        if (this.state.volume !== volume) {
893
-            this.setState({ volume });
894
-        }
895
-    }
896
-
897 870
     _onVolumeChange: number => void;
898 871
 
899 872
     /**
@@ -903,7 +876,10 @@ class Thumbnail extends Component<Props, State> {
903 876
      * @returns {void}
904 877
      */
905 878
     _onVolumeChange(value) {
906
-        this.setState({ volume: value });
879
+        const { _participant, dispatch } = this.props;
880
+        const { id } = _participant;
881
+
882
+        dispatch(setVolume(id, value));
907 883
     }
908 884
 
909 885
     /**
@@ -949,6 +925,7 @@ function _mapStateToProps(state, ownProps): Object {
949 925
     const { id } = participant;
950 926
     const isLocal = participant?.local ?? true;
951 927
     const tracks = state['features/base/tracks'];
928
+    const { participantsVolume } = state['features/filmstrip'];
952 929
     const _videoTrack = isLocal
953 930
         ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
954 931
     const _audioTrack = isLocal
@@ -967,14 +944,21 @@ function _mapStateToProps(state, ownProps): Object {
967 944
 
968 945
 
969 946
     switch (_currentLayout) {
947
+    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
970 948
     case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
971 949
         const {
972 950
             horizontalViewDimensions = {
973 951
                 local: {},
974 952
                 remote: {}
953
+            },
954
+            verticalViewDimensions = {
955
+                local: {},
956
+                remote: {}
975 957
             }
976 958
         } = state['features/filmstrip'];
977
-        const { local, remote } = horizontalViewDimensions;
959
+        const { local, remote }
960
+            = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
961
+                ? verticalViewDimensions : horizontalViewDimensions;
978 962
         const { width, height } = isLocal ? local : remote;
979 963
 
980 964
         size = {
@@ -984,13 +968,6 @@ function _mapStateToProps(state, ownProps): Object {
984 968
 
985 969
         break;
986 970
     }
987
-    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
988
-        size = {
989
-            _heightToWidthPercent: isLocal
990
-                ? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
991
-                : 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
992
-        };
993
-        break;
994 971
     case LAYOUTS.TILE_VIEW: {
995 972
         const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
996 973
 
@@ -1020,9 +997,10 @@ function _mapStateToProps(state, ownProps): Object {
1020 997
         _indicatorIconSize: NORMAL,
1021 998
         _localFlipX: Boolean(localFlipX),
1022 999
         _participant: participant,
1023
-        _participantCount: getParticipantCount(state),
1000
+        _participantCountMoreThan2: getParticipantCount(state) > 2,
1024 1001
         _startSilent: Boolean(startSilent),
1025 1002
         _videoTrack,
1003
+        _volume: isLocal ? undefined : participantsVolume[id],
1026 1004
         ...size
1027 1005
     };
1028 1006
 }

+ 155
- 0
react/features/filmstrip/components/web/ThumbnailWrapper.js View File

@@ -0,0 +1,155 @@
1
+/* @flow */
2
+import React, { Component } from 'react';
3
+import { shouldComponentUpdate } from 'react-window';
4
+
5
+import { connect } from '../../../base/redux';
6
+import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
7
+
8
+import Thumbnail from './Thumbnail';
9
+
10
+/**
11
+ * The type of the React {@code Component} props of {@link ThumbnailWrapper}.
12
+ */
13
+type Props = {
14
+
15
+    /**
16
+     * The ID of the participant associated with the Thumbnail.
17
+     */
18
+    _participantID: ?string,
19
+
20
+    /**
21
+     * The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
22
+     */
23
+    _horizontalOffset: number,
24
+
25
+    /**
26
+     * The index of the column in tile view.
27
+     */
28
+    columnIndex?: number,
29
+
30
+    /**
31
+     * The index of the ThumbnailWrapper in stage view.
32
+     */
33
+    index?: number,
34
+
35
+    /**
36
+     * The index of the row in tile view.
37
+     */
38
+    rowIndex?: number,
39
+
40
+    /**
41
+     * The styles comming from react-window.
42
+     */
43
+    style: Object
44
+};
45
+
46
+/**
47
+ * A wrapper Component for the Thumbnail that translates the react-window specific props
48
+ * to the Thumbnail Component's props.
49
+ */
50
+class ThumbnailWrapper extends Component<Props> {
51
+
52
+    /**
53
+     * Creates new ThumbnailWrapper instance.
54
+     *
55
+     * @param {Props} props - The props of the component.
56
+     */
57
+    constructor(props: Props) {
58
+        super(props);
59
+
60
+        this.shouldComponentUpdate = shouldComponentUpdate.bind(this);
61
+    }
62
+
63
+    shouldComponentUpdate: Props => boolean;
64
+
65
+    /**
66
+     * Implements React's {@link Component#render()}.
67
+     *
68
+     * @inheritdoc
69
+     * @returns {ReactElement}
70
+     */
71
+    render() {
72
+        const { _participantID, style, _horizontalOffset = 0 } = this.props;
73
+
74
+        if (typeof _participantID !== 'string') {
75
+            return null;
76
+        }
77
+
78
+        if (_participantID === 'local') {
79
+            return (
80
+                <Thumbnail
81
+                    horizontalOffset = { _horizontalOffset }
82
+                    key = 'local'
83
+                    style = { style } />);
84
+        }
85
+
86
+        return (
87
+            <Thumbnail
88
+                horizontalOffset = { _horizontalOffset }
89
+                key = { `remote_${_participantID}` }
90
+                participantID = { _participantID }
91
+                style = { style } />);
92
+    }
93
+}
94
+
95
+/**
96
+ * Maps (parts of) the Redux state to the associated {@code ThumbnailWrapper}'s props.
97
+ *
98
+ * @param {Object} state - The Redux state.
99
+ * @param {Object} ownProps - The props passed to the component.
100
+ * @private
101
+ * @returns {Props}
102
+ */
103
+function _mapStateToProps(state, ownProps) {
104
+    const _currentLayout = getCurrentLayout(state);
105
+    const { remoteParticipants } = state['features/filmstrip'];
106
+    const remoteParticipantsLength = remoteParticipants.length;
107
+
108
+    if (_currentLayout === LAYOUTS.TILE_VIEW) {
109
+        const { columnIndex, rowIndex } = ownProps;
110
+        const { gridDimensions = {}, thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
111
+        const { columns, rows } = gridDimensions;
112
+        const index = (rowIndex * columns) + columnIndex;
113
+        let horizontalOffset;
114
+
115
+        if (rowIndex === rows - 1) { // center the last row
116
+            const { width: thumbnailWidth } = thumbnailSize;
117
+            const participantsInTheLastRow = (remoteParticipantsLength + 1) % columns;
118
+
119
+            if (participantsInTheLastRow > 0) {
120
+                horizontalOffset = Math.floor((columns - participantsInTheLastRow) * (thumbnailWidth + 4) / 2);
121
+            }
122
+
123
+        }
124
+
125
+        if (index > remoteParticipantsLength) {
126
+            return {};
127
+        }
128
+
129
+        if (index === remoteParticipantsLength) {
130
+            return {
131
+                _participantID: 'local',
132
+                _horizontalOffset: horizontalOffset
133
+            };
134
+        }
135
+
136
+
137
+        return {
138
+            _participantID: remoteParticipants[index],
139
+            _horizontalOffset: horizontalOffset
140
+        };
141
+
142
+    }
143
+
144
+    const { index } = ownProps;
145
+
146
+    if (typeof index !== 'number' || remoteParticipantsLength <= index) {
147
+        return {};
148
+    }
149
+
150
+    return {
151
+        _participantID: remoteParticipants[index]
152
+    };
153
+}
154
+
155
+export default connect(_mapStateToProps)(ThumbnailWrapper);

+ 65
- 0
react/features/filmstrip/constants.js View File

@@ -143,3 +143,68 @@ export const DISPLAY_MODE_TO_STRING = [
143 143
     'video-with-name',
144 144
     'avatar-with-name'
145 145
 ];
146
+
147
+/**
148
+ * The vertical margin of a tile.
149
+ *
150
+ * @type {number}
151
+ */
152
+export const TILE_VERTICAL_MARGIN = 4;
153
+
154
+/**
155
+ * The horizontal margin of a tile.
156
+ *
157
+ * @type {number}
158
+ */
159
+export const TILE_HORIZONTAL_MARGIN = 4;
160
+
161
+/**
162
+ * The height of the whole toolbar.
163
+ */
164
+export const TOOLBAR_HEIGHT = 72;
165
+
166
+/**
167
+ * The size of the horizontal border of a thumbnail.
168
+ *
169
+ * @type {number}
170
+ */
171
+export const STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER = 4;
172
+
173
+/**
174
+ * The size of the vertical border of a thumbnail.
175
+ *
176
+ * @type {number}
177
+ */
178
+export const STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER = 4;
179
+
180
+/**
181
+ * The size of the scroll.
182
+ *
183
+ * @type {number}
184
+ */
185
+export const SCROLL_SIZE = 7;
186
+
187
+/**
188
+ * The total vertical space between the thumbnails container and the edges of the window.
189
+ *
190
+ * NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
191
+ *
192
+ * @type {number}
193
+ */
194
+export const VERTICAL_FILMSTRIP_VERTICAL_MARGIN = 60;
195
+
196
+/**
197
+ * The min horizontal space between the thumbnails container and the edges of the window.
198
+ *
199
+ * @type {number}
200
+ */
201
+export const VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN = 10;
202
+
203
+/**
204
+ * The total horizontal space between the thumbnails container and the edges of the window.
205
+ *
206
+ * NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
207
+ *
208
+ * @type {number}
209
+ */
210
+export const HORIZONTAL_FILMSTRIP_MARGIN = 39;

+ 56
- 11
react/features/filmstrip/functions.web.js View File

@@ -23,16 +23,17 @@ import {
23 23
     DISPLAY_BLACKNESS_WITH_NAME,
24 24
     DISPLAY_VIDEO,
25 25
     DISPLAY_VIDEO_WITH_NAME,
26
+    SCROLL_SIZE,
26 27
     SQUARE_TILE_ASPECT_RATIO,
27
-    TILE_ASPECT_RATIO
28
+    STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
29
+    TILE_ASPECT_RATIO,
30
+    TILE_HORIZONTAL_MARGIN,
31
+    TILE_VERTICAL_MARGIN,
32
+    VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
28 33
 } from './constants';
29 34
 
30 35
 declare var interfaceConfig: Object;
31 36
 
32
-// Minimum space to keep between the sides of the tiles and the sides
33
-// of the window.
34
-const TILE_VIEW_SIDE_MARGINS = 20;
35
-
36 37
 /**
37 38
  * Returns true if the filmstrip on mobile is visible, false otherwise.
38 39
  *
@@ -139,15 +140,42 @@ export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0
139 140
     };
140 141
 }
141 142
 
143
+/**
144
+ * Calculates the size for thumbnails when in vertical view layout.
145
+ *
146
+ * @param {number} clientWidth - The height of the app window.
147
+ * @returns {{local: {height, width}, remote: {height, width}}}
148
+ */
149
+export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0) {
150
+    const horizontalMargin
151
+        = VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN + SCROLL_SIZE
152
+            + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;
153
+    const availableWidth = Math.min(
154
+        Math.max(clientWidth - horizontalMargin, 0),
155
+        interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120);
156
+
157
+    return {
158
+        local: {
159
+            height: Math.floor(availableWidth / interfaceConfig.LOCAL_THUMBNAIL_RATIO),
160
+            width: availableWidth
161
+        },
162
+        remote: {
163
+            height: Math.floor(availableWidth / interfaceConfig.REMOTE_THUMBNAIL_RATIO),
164
+            width: availableWidth
165
+        }
166
+    };
167
+}
168
+
142 169
 /**
143 170
  * Calculates the size for thumbnails when in tile view layout.
144 171
  *
145 172
  * @param {Object} dimensions - The desired dimensions of the tile view grid.
146
- * @returns {{height, width}}
173
+ * @returns {{hasScroll, height, width}}
147 174
  */
148 175
 export function calculateThumbnailSizeForTileView({
149 176
     columns,
150
-    visibleRows,
177
+    minVisibleRows,
178
+    rows,
151 179
     clientWidth,
152 180
     clientHeight,
153 181
     disableResponsiveTiles
@@ -158,12 +186,29 @@ export function calculateThumbnailSizeForTileView({
158 186
         aspectRatio = SQUARE_TILE_ASPECT_RATIO;
159 187
     }
160 188
 
161
-    const viewWidth = clientWidth - TILE_VIEW_SIDE_MARGINS;
162
-    const viewHeight = clientHeight - TILE_VIEW_SIDE_MARGINS;
189
+    const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN);
190
+    const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
163 191
     const initialWidth = viewWidth / columns;
192
+    const initialHeight = viewHeight / minVisibleRows;
164 193
     const aspectRatioHeight = initialWidth / aspectRatio;
165
-    const height = Math.floor(Math.min(aspectRatioHeight, viewHeight / visibleRows));
166
-    const width = Math.floor(aspectRatio * height);
194
+    const noScrollHeight = (clientHeight / rows) - TILE_VERTICAL_MARGIN;
195
+    const scrollInitialWidth = (viewWidth - SCROLL_SIZE) / columns;
196
+    let height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
197
+    let width = Math.floor(aspectRatio * height);
198
+
199
+    if (height > noScrollHeight && width > scrollInitialWidth) { // we will have scroll and we need more space for it.
200
+        const scrollAspectRatioHeight = scrollInitialWidth / aspectRatio;
201
+
202
+        // Recalculating width/height to fit the available space when a scroll is displayed.
203
+        // NOTE: Math.min(scrollAspectRatioHeight, initialHeight) would be enough to recalculate but since the new
204
+        // height value can theoretically be dramatically smaller and the scroll may not be neccessary anymore we need
205
+        // to compare it with noScrollHeight( the optimal height to fit all thumbnails without scroll) and get the
206
+        // bigger one. This way we ensure that we always strech the thumbnails as close as we can to the edges of the
207
+        // window.
208
+        height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
209
+        width = Math.floor(aspectRatio * height);
210
+    }
211
+
167 212
 
168 213
     return {
169 214
         height,

+ 8
- 14
react/features/filmstrip/middleware.web.js View File

@@ -9,7 +9,7 @@ import {
9 9
     LAYOUTS
10 10
 } from '../video-layout';
11 11
 
12
-import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
12
+import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
13 13
 
14 14
 import './subscriber.web';
15 15
 
@@ -27,22 +27,16 @@ MiddlewareRegistry.register(store => next => action => {
27 27
         switch (layout) {
28 28
         case LAYOUTS.TILE_VIEW: {
29 29
             const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
30
-            const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
31
-
32
-            store.dispatch(
33
-                setTileViewDimensions(
34
-                    gridDimensions,
35
-                    {
36
-                        clientHeight,
37
-                        clientWidth
38
-                    },
39
-                    store
40
-                )
41
-            );
30
+
31
+            store.dispatch(setTileViewDimensions(gridDimensions));
42 32
             break;
43 33
         }
44 34
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
45
-            store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
35
+            store.dispatch(setHorizontalViewDimensions());
36
+            break;
37
+
38
+        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
39
+            store.dispatch(setVerticalViewDimensions());
46 40
             break;
47 41
         }
48 42
         break;

+ 65
- 1
react/features/filmstrip/reducer.js View File

@@ -1,12 +1,15 @@
1 1
 // @flow
2 2
 
3
+import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
3 4
 import { ReducerRegistry } from '../base/redux';
4 5
 
5 6
 import {
6 7
     SET_FILMSTRIP_ENABLED,
7 8
     SET_FILMSTRIP_VISIBLE,
8 9
     SET_HORIZONTAL_VIEW_DIMENSIONS,
9
-    SET_TILE_VIEW_DIMENSIONS
10
+    SET_TILE_VIEW_DIMENSIONS,
11
+    SET_VERTICAL_VIEW_DIMENSIONS,
12
+    SET_VOLUME
10 13
 } from './actionTypes';
11 14
 
12 15
 const DEFAULT_STATE = {
@@ -26,6 +29,21 @@ const DEFAULT_STATE = {
26 29
      */
27 30
     horizontalViewDimensions: {},
28 31
 
32
+    /**
33
+     * The custom audio volume levels per perticipant.
34
+     *
35
+     * @type {Object}
36
+     */
37
+    participantsVolume: {},
38
+
39
+    /**
40
+     * The ordered IDs of the remote participants displayed in the filmstrip.
41
+     *
42
+     * NOTE: Currently the order will match the one from the base/participants array. But this is good initial step for
43
+     * reordering the remote participants.
44
+     */
45
+    remoteParticipants: [],
46
+
29 47
     /**
30 48
      * The tile view dimensions.
31 49
      *
@@ -34,6 +52,14 @@ const DEFAULT_STATE = {
34 52
      */
35 53
     tileViewDimensions: {},
36 54
 
55
+    /**
56
+     * The vertical view dimensions.
57
+     *
58
+     * @public
59
+     * @type {Object}
60
+     */
61
+    verticalViewDimensions: {},
62
+
37 63
     /**
38 64
      * The indicator which determines whether the {@link Filmstrip} is visible.
39 65
      *
@@ -69,6 +95,44 @@ ReducerRegistry.register(
69 95
                 ...state,
70 96
                 tileViewDimensions: action.dimensions
71 97
             };
98
+        case SET_VERTICAL_VIEW_DIMENSIONS:
99
+            return {
100
+                ...state,
101
+                verticalViewDimensions: action.dimensions
102
+            };
103
+        case SET_VOLUME:
104
+            return {
105
+                ...state,
106
+                participantsVolume: {
107
+                    ...state.participantsVolume,
108
+
109
+                    // NOTE: This would fit better in the features/base/participants. But currently we store
110
+                    // the participants as an array which will make it expensive to search for the volume for
111
+                    // every participant separately.
112
+                    [action.participantId]: action.volume
113
+                }
114
+            };
115
+        case PARTICIPANT_JOINED: {
116
+            const { id, local } = action.participant;
117
+
118
+            if (!local) {
119
+                state.remoteParticipants = [ ...state.remoteParticipants, id ];
120
+            }
121
+
122
+            return state;
123
+        }
124
+        case PARTICIPANT_LEFT: {
125
+            const { id, local } = action.participant;
126
+
127
+            if (local) {
128
+                return state;
129
+            }
130
+
131
+            state.remoteParticipants = state.remoteParticipants.filter(participantId => participantId !== id);
132
+            delete state.participantsVolume[id];
133
+
134
+            return state;
135
+        }
72 136
         }
73 137
 
74 138
         return state;

+ 10
- 40
react/features/filmstrip/subscriber.web.js View File

@@ -7,7 +7,7 @@ import { getParticipantsPaneOpen } from '../participants-pane/functions';
7 7
 import { setOverflowDrawer } from '../toolbox/actions.web';
8 8
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
9 9
 
10
-import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
10
+import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
11 11
 import {
12 12
     ASPECT_RATIO_BREAKPOINT,
13 13
     DISPLAY_DRAWER_THRESHOLD,
@@ -28,18 +28,7 @@ StateListenerRegistry.register(
28 28
             const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
29 29
 
30 30
             if (!equals(gridDimensions, oldGridDimensions)) {
31
-                const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
32
-
33
-                store.dispatch(
34
-                    setTileViewDimensions(
35
-                        gridDimensions,
36
-                        {
37
-                            clientHeight,
38
-                            clientWidth
39
-                        },
40
-                        store
41
-                    )
42
-                );
31
+                store.dispatch(setTileViewDimensions(gridDimensions));
43 32
             }
44 33
         }
45 34
     });
@@ -53,23 +42,14 @@ StateListenerRegistry.register(
53 42
         const state = store.getState();
54 43
 
55 44
         switch (layout) {
56
-        case LAYOUTS.TILE_VIEW: {
57
-            const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
58
-
59
-            store.dispatch(
60
-                setTileViewDimensions(
61
-                    getTileViewGridDimensions(state),
62
-                    {
63
-                        clientHeight,
64
-                        clientWidth
65
-                    },
66
-                    store
67
-                )
68
-            );
45
+        case LAYOUTS.TILE_VIEW:
46
+            store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
69 47
             break;
70
-        }
71 48
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
72
-            store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
49
+            store.dispatch(setHorizontalViewDimensions());
50
+            break;
51
+        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
52
+            store.dispatch(setVerticalViewDimensions());
73 53
             break;
74 54
         }
75 55
     });
@@ -168,17 +148,7 @@ StateListenerRegistry.register(
168 148
 
169 149
         if (shouldDisplayTileView(state)) {
170 150
             const gridDimensions = getTileViewGridDimensions(state);
171
-            const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
172
-
173
-            store.dispatch(
174
-                setTileViewDimensions(
175
-                    gridDimensions,
176
-                    {
177
-                        clientHeight,
178
-                        clientWidth
179
-                    },
180
-                    store
181
-                )
182
-            );
151
+
152
+            store.dispatch(setTileViewDimensions(gridDimensions));
183 153
         }
184 154
     });

+ 3
- 2
react/features/video-layout/functions.js View File

@@ -106,11 +106,12 @@ export function getTileViewGridDimensions(state: Object) {
106 106
     const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
107 107
     const columns = Math.min(columnsToMaintainASquare, maxColumns);
108 108
     const rows = Math.ceil(numberOfParticipants / columns);
109
-    const visibleRows = Math.min(maxColumns, rows);
109
+    const minVisibleRows = Math.min(maxColumns, rows);
110 110
 
111 111
     return {
112 112
         columns,
113
-        visibleRows
113
+        minVisibleRows,
114
+        rows
114 115
     };
115 116
 }
116 117
 

Loading…
Cancel
Save