Browse Source

feat(filmstrip-pagination): mobile support.

master
Hristo Terezov 4 years ago
parent
commit
7dd43d93b6

+ 1
- 1
react/features/base/conference/middleware.any.js View File

551
 
551
 
552
     const localParticipant = getLocalParticipant(getState);
552
     const localParticipant = getLocalParticipant(getState);
553
 
553
 
554
-    if (conference && participant.id === localParticipant.id) {
554
+    if (conference && participant.id === localParticipant?.id) {
555
         if ('name' in participant) {
555
         if ('name' in participant) {
556
             conference.setDisplayName(participant.name);
556
             conference.setDisplayName(participant.name);
557
         }
557
         }

+ 4
- 3
react/features/base/responsive-ui/actions.js View File

29
  */
29
  */
30
 export function clientResized(clientWidth: number, clientHeight: number) {
30
 export function clientResized(clientWidth: number, clientHeight: number) {
31
     return (dispatch: Dispatch<any>, getState: Function) => {
31
     return (dispatch: Dispatch<any>, getState: Function) => {
32
-        const state = getState();
33
-        const { isOpen: isChatOpen } = state['features/chat'];
34
-        const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
35
         let availableWidth = clientWidth;
32
         let availableWidth = clientWidth;
36
 
33
 
37
         if (navigator.product !== 'ReactNative') {
34
         if (navigator.product !== 'ReactNative') {
35
+            const state = getState();
36
+            const { isOpen: isChatOpen } = state['features/chat'];
37
+            const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
38
+
38
             if (isChatOpen) {
39
             if (isChatOpen) {
39
                 availableWidth -= CHAT_SIZE;
40
                 availableWidth -= CHAT_SIZE;
40
             }
41
             }

+ 26
- 1
react/features/filmstrip/actions.any.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
-import { SET_FILMSTRIP_ENABLED, SET_FILMSTRIP_VISIBLE, SET_REMOTE_PARTICIPANTS } from './actionTypes';
3
+import {
4
+    SET_FILMSTRIP_ENABLED,
5
+    SET_FILMSTRIP_VISIBLE,
6
+    SET_REMOTE_PARTICIPANTS,
7
+    SET_VISIBLE_REMOTE_PARTICIPANTS
8
+} from './actionTypes';
4
 
9
 
5
 /**
10
 /**
6
  * Sets whether the filmstrip is enabled.
11
  * Sets whether the filmstrip is enabled.
50
         participants
55
         participants
51
     };
56
     };
52
 }
57
 }
58
+
59
+/**
60
+ * Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
61
+ * participants array.
62
+ *
63
+ * @param {number} startIndex - The start index from the remote participants array.
64
+ * @param {number} endIndex - The end index from the remote participants array.
65
+ * @returns {{
66
+ *      type: SET_VISIBLE_REMOTE_PARTICIPANTS,
67
+ *      startIndex: number,
68
+ *      endIndex: number
69
+ * }}
70
+ */
71
+export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
72
+    return {
73
+        type: SET_VISIBLE_REMOTE_PARTICIPANTS,
74
+        startIndex,
75
+        endIndex
76
+    };
77
+}

+ 37
- 13
react/features/filmstrip/actions.native.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { getParticipantCountWithFake } from '../base/participants';
4
+
3
 import { SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
5
 import { SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
6
+import { SQUARE_TILE_ASPECT_RATIO, TILE_MARGIN } from './constants';
7
+import { getColumnCount } from './functions.native';
4
 
8
 
5
 export * from './actions.any';
9
 export * from './actions.any';
6
 
10
 
9
  * of the values are currently used. Check the description of {@link SET_TILE_VIEW_DIMENSIONS} for the full set
13
  * of the values are currently used. Check the description of {@link SET_TILE_VIEW_DIMENSIONS} for the full set
10
  * of properties.
14
  * of properties.
11
  *
15
  *
12
- * @param {Object} dimensions - The tile view dimensions.
13
- * @param {Object} thumbnailSize - The size of an individual video thumbnail.
14
- * @param {number} thumbnailSize.height - The height of an individual video thumbnail.
15
- * @param {number} thumbnailSize.width - The width of an individual video thumbnail.
16
- * @returns {{
17
- *     type: SET_TILE_VIEW_DIMENSIONS,
18
- *     dimensions: Object
19
- * }}
16
+ * @returns {Function}
20
  */
17
  */
21
-export function setTileViewDimensions({ thumbnailSize }: Object) {
22
-    return {
23
-        type: SET_TILE_VIEW_DIMENSIONS,
24
-        dimensions: {
25
-            thumbnailSize
18
+export function setTileViewDimensions() {
19
+    return (dispatch: Function, getState: Function) => {
20
+        const state = getState();
21
+        const participantCount = getParticipantCountWithFake(state);
22
+        const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
23
+        const columns = getColumnCount(state);
24
+        const heightToUse = height - (TILE_MARGIN * 2);
25
+        const widthToUse = width - (TILE_MARGIN * 2);
26
+        let tileWidth;
27
+
28
+        // If there is going to be at least two rows, ensure that at least two
29
+        // rows display fully on screen.
30
+        if (participantCount / columns > 1) {
31
+            tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
32
+        } else {
33
+            tileWidth = Math.min(widthToUse / columns, heightToUse);
26
         }
34
         }
35
+
36
+        const tileHeight = Math.floor(tileWidth / SQUARE_TILE_ASPECT_RATIO);
37
+
38
+        tileWidth = Math.floor(tileWidth);
39
+
40
+
41
+        dispatch({
42
+            type: SET_TILE_VIEW_DIMENSIONS,
43
+            dimensions: {
44
+                columns,
45
+                thumbnailSize: {
46
+                    height: tileHeight,
47
+                    width: tileWidth
48
+                }
49
+            }
50
+        });
27
     };
51
     };
28
 }
52
 }

+ 0
- 21
react/features/filmstrip/actions.web.js View File

7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
8
     SET_TILE_VIEW_DIMENSIONS,
8
     SET_TILE_VIEW_DIMENSIONS,
9
     SET_VERTICAL_VIEW_DIMENSIONS,
9
     SET_VERTICAL_VIEW_DIMENSIONS,
10
-    SET_VISIBLE_REMOTE_PARTICIPANTS,
11
     SET_VOLUME
10
     SET_VOLUME
12
 } from './actionTypes';
11
 } from './actionTypes';
13
 import {
12
 import {
159
         volume
158
         volume
160
     };
159
     };
161
 }
160
 }
162
-
163
-/**
164
- * Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
165
- * participants array.
166
- *
167
- * @param {number} startIndex - The start index from the remote participants array.
168
- * @param {number} endIndex - The end index from the remote participants array.
169
- * @returns {{
170
- *      type: SET_VISIBLE_REMOTE_PARTICIPANTS,
171
- *      startIndex: number,
172
- *      endIndex: number
173
- * }}
174
- */
175
-export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
176
-    return {
177
-        type: SET_VISIBLE_REMOTE_PARTICIPANTS,
178
-        startIndex,
179
-        endIndex
180
-    };
181
-}

+ 146
- 54
react/features/filmstrip/components/native/Filmstrip.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { Component } from 'react';
4
-import { SafeAreaView, ScrollView } from 'react-native';
3
+import React, { PureComponent } from 'react';
4
+import { FlatList, SafeAreaView } from 'react-native';
5
 
5
 
6
+import { getLocalParticipant } from '../../../base/participants';
6
 import { Platform } from '../../../base/react';
7
 import { Platform } from '../../../base/react';
7
 import { connect } from '../../../base/redux';
8
 import { connect } from '../../../base/redux';
8
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
9
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
10
+import { setVisibleRemoteParticipants } from '../../actions';
9
 import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
11
 import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
10
 
12
 
11
 import LocalThumbnail from './LocalThumbnail';
13
 import LocalThumbnail from './LocalThumbnail';
25
      */
27
      */
26
     _aspectRatio: Symbol,
28
     _aspectRatio: Symbol,
27
 
29
 
30
+    _clientWidth: number,
31
+
32
+    _clientHeight: number,
33
+
34
+    _localParticipantId: string,
35
+
28
     /**
36
     /**
29
      * The participants in the conference.
37
      * The participants in the conference.
30
      */
38
      */
33
     /**
41
     /**
34
      * The indicator which determines whether the filmstrip is visible.
42
      * The indicator which determines whether the filmstrip is visible.
35
      */
43
      */
36
-    _visible: boolean
44
+    _visible: boolean,
45
+
46
+    /**
47
+     * Invoked to trigger state changes in Redux.
48
+     */
49
+    dispatch: Function,
37
 };
50
 };
38
 
51
 
39
 /**
52
 /**
42
  *
55
  *
43
  * @extends Component
56
  * @extends Component
44
  */
57
  */
45
-class Filmstrip extends Component<Props> {
58
+class Filmstrip extends PureComponent<Props> {
46
     /**
59
     /**
47
      * Whether the local participant should be rendered separately from the
60
      * Whether the local participant should be rendered separately from the
48
      * remote participants i.e. outside of their {@link ScrollView}.
61
      * remote participants i.e. outside of their {@link ScrollView}.
49
      */
62
      */
50
     _separateLocalThumbnail: boolean;
63
     _separateLocalThumbnail: boolean;
51
 
64
 
65
+    /**
66
+     * The FlatList's viewabilityConfig.
67
+     */
68
+    _viewabilityConfig: Object;
69
+
52
     /**
70
     /**
53
      * Constructor of the component.
71
      * Constructor of the component.
54
      *
72
      *
75
         // do not have much of a choice but to continue rendering LocalThumbnail
93
         // do not have much of a choice but to continue rendering LocalThumbnail
76
         // as any other remote Thumbnail on Android.
94
         // as any other remote Thumbnail on Android.
77
         this._separateLocalThumbnail = Platform.OS !== 'android';
95
         this._separateLocalThumbnail = Platform.OS !== 'android';
96
+
97
+        this._viewabilityConfig = {
98
+            itemVisiblePercentThreshold: 30
99
+        };
100
+        this._keyExtractor = this._keyExtractor.bind(this);
101
+        this._getItemLayout = this._getItemLayout.bind(this);
102
+        this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
103
+        this._renderThumbnail = this._renderThumbnail.bind(this);
104
+    }
105
+
106
+    _keyExtractor: string => string;
107
+
108
+    /**
109
+     * Returns a key for a passed item of the list.
110
+     *
111
+     * @param {string} item - The user ID.
112
+     * @returns {string} - The user ID.
113
+     */
114
+    _keyExtractor(item) {
115
+        return item;
116
+    }
117
+
118
+    /**
119
+     * Calculates the width and height of the filmstrip based on the screen size and aspect ratio.
120
+     *
121
+     * @returns {Object} - The width and the height.
122
+     */
123
+    _getDimensions() {
124
+        const { _aspectRatio, _clientWidth, _clientHeight } = this.props;
125
+        const { height, width, margin } = styles.thumbnail;
126
+
127
+        if (_aspectRatio === ASPECT_RATIO_NARROW) {
128
+            return {
129
+                height,
130
+                width: this._separateLocalThumbnail ? _clientWidth - width - (margin * 2) : _clientWidth
131
+            };
132
+        }
133
+
134
+        return {
135
+            height: this._separateLocalThumbnail ? _clientHeight - height - (margin * 2) : _clientHeight,
136
+            width
137
+        };
138
+    }
139
+
140
+    _getItemLayout: (?Array<string>, number) => {length: number, offset: number, index: number};
141
+
142
+    /**
143
+     * Optimization for FlatList. Returns the length, offset and index for an item.
144
+     *
145
+     * @param {Array<string>} data - The data array with user IDs.
146
+     * @param {number} index - The index number of the item.
147
+     * @returns {Object}
148
+     */
149
+    _getItemLayout(data, index) {
150
+        const { _aspectRatio } = this.props;
151
+        const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
152
+        const length = isNarrowAspectRatio ? styles.thumbnail.width : styles.thumbnail.height;
153
+
154
+        return {
155
+            length,
156
+            offset: length * index,
157
+            index
158
+        };
159
+    }
160
+
161
+    _onViewableItemsChanged: Object => void;
162
+
163
+    /**
164
+     * A handler for visible items changes.
165
+     *
166
+     * @param {Object} data - The visible items data.
167
+     * @param {Array<Object>} data.viewableItems - The visible items array.
168
+     * @returns {void}
169
+     */
170
+    _onViewableItemsChanged({ viewableItems = [] }) {
171
+        const indexArray: Array<number> = viewableItems.map(i => i.index);
172
+
173
+        // If the local video placed at the beginning we need to shift the start index of the remoteParticipants array
174
+        // with 1 because and in the same time we don't need to adjust the end index because the end index will not be
175
+        // included.
176
+        const startIndex
177
+            = this._separateLocalThumbnail ? Math.min(...indexArray) : Math.max(Math.min(...indexArray) - 1, 0);
178
+        const endIndex = Math.max(...indexArray) + (this._separateLocalThumbnail ? 1 : 0);
179
+
180
+        this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
181
+    }
182
+
183
+    _renderThumbnail: Object => Object;
184
+
185
+    /**
186
+     * Creates React Element to display each participant in a thumbnail.
187
+     *
188
+     * @private
189
+     * @returns {ReactElement}
190
+     */
191
+    _renderThumbnail({ item /* , index , separators */ }) {
192
+        return (
193
+            <Thumbnail
194
+                key = { item }
195
+                participantID = { item } />)
196
+        ;
78
     }
197
     }
79
 
198
 
80
     /**
199
     /**
84
      * @returns {ReactElement}
203
      * @returns {ReactElement}
85
      */
204
      */
86
     render() {
205
     render() {
87
-        const { _aspectRatio, _participants, _visible } = this.props;
206
+        const { _aspectRatio, _localParticipantId, _participants, _visible } = this.props;
88
 
207
 
89
         if (!_visible) {
208
         if (!_visible) {
90
             return null;
209
             return null;
92
 
211
 
93
         const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
212
         const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
94
         const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
213
         const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
214
+        const { height, width } = this._getDimensions();
215
+        const { height: thumbnailHeight, width: thumbnailWidth, margin } = styles.thumbnail;
216
+        const initialNumToRender = Math.ceil(isNarrowAspectRatio
217
+            ? width / (thumbnailWidth + (2 * margin))
218
+            : height / (thumbnailHeight + (2 * margin))
219
+        );
220
+        const participants = this._separateLocalThumbnail ? _participants : [ _localParticipantId, ..._participants ];
95
 
221
 
96
         return (
222
         return (
97
             <SafeAreaView style = { filmstripStyle }>
223
             <SafeAreaView style = { filmstripStyle }>
100
                         && !isNarrowAspectRatio
226
                         && !isNarrowAspectRatio
101
                         && <LocalThumbnail />
227
                         && <LocalThumbnail />
102
                 }
228
                 }
103
-                <ScrollView
229
+                <FlatList
230
+                    data = { participants }
231
+                    getItemLayout = { this._getItemLayout }
104
                     horizontal = { isNarrowAspectRatio }
232
                     horizontal = { isNarrowAspectRatio }
233
+                    initialNumToRender = { initialNumToRender }
234
+                    key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
235
+                    keyExtractor = { this._keyExtractor }
236
+                    onViewableItemsChanged = { this._onViewableItemsChanged }
237
+                    renderItem = { this._renderThumbnail }
105
                     showsHorizontalScrollIndicator = { false }
238
                     showsHorizontalScrollIndicator = { false }
106
                     showsVerticalScrollIndicator = { false }
239
                     showsVerticalScrollIndicator = { false }
107
-                    style = { styles.scrollView } >
108
-                    {
109
-                        !this._separateLocalThumbnail && !isNarrowAspectRatio
110
-                            && <LocalThumbnail />
111
-                    }
112
-                    {
113
-
114
-                        this._sort(_participants, isNarrowAspectRatio)
115
-                            .map(id => (
116
-                                <Thumbnail
117
-                                    key = { id }
118
-                                    participantID = { id } />))
119
-
120
-                    }
121
-                    {
122
-                        !this._separateLocalThumbnail && isNarrowAspectRatio
123
-                            && <LocalThumbnail />
124
-                    }
125
-                </ScrollView>
240
+                    style = { styles.scrollView }
241
+                    viewabilityConfig = { this._viewabilityConfig }
242
+                    windowSize = { 2 } />
126
                 {
243
                 {
127
                     this._separateLocalThumbnail && isNarrowAspectRatio
244
                     this._separateLocalThumbnail && isNarrowAspectRatio
128
                         && <LocalThumbnail />
245
                         && <LocalThumbnail />
130
             </SafeAreaView>
247
             </SafeAreaView>
131
         );
248
         );
132
     }
249
     }
133
-
134
-    /**
135
-     * Sorts a specific array of {@code Participant}s in display order.
136
-     *
137
-     * @param {Participant[]} participants - The array of {@code Participant}s
138
-     * to sort in display order.
139
-     * @param {boolean} isNarrowAspectRatio - Indicates if the aspect ratio is
140
-     * wide or narrow.
141
-     * @private
142
-     * @returns {Participant[]} A new array containing the elements of the
143
-     * specified {@code participants} array sorted in display order.
144
-     */
145
-    _sort(participants, isNarrowAspectRatio) {
146
-        // XXX Array.prototype.sort() is not appropriate because (1) it operates
147
-        // in place and (2) it is not necessarily stable.
148
-
149
-        const sortedParticipants = [
150
-            ...participants
151
-        ];
152
-
153
-        if (isNarrowAspectRatio) {
154
-            // When the narrow aspect ratio is used, we want to have the remote
155
-            // participants from right to left with the newest added/joined to
156
-            // the leftmost side. The local participant is the leftmost item.
157
-            sortedParticipants.reverse();
158
-        }
159
-
160
-        return sortedParticipants;
161
-    }
162
 }
250
 }
163
 
251
 
164
 /**
252
 /**
171
 function _mapStateToProps(state) {
259
 function _mapStateToProps(state) {
172
     const { enabled, remoteParticipants } = state['features/filmstrip'];
260
     const { enabled, remoteParticipants } = state['features/filmstrip'];
173
     const showRemoteVideos = shouldRemoteVideosBeVisible(state);
261
     const showRemoteVideos = shouldRemoteVideosBeVisible(state);
262
+    const responsiveUI = state['features/base/responsive-ui'];
174
 
263
 
175
     return {
264
     return {
176
         _aspectRatio: state['features/base/responsive-ui'].aspectRatio,
265
         _aspectRatio: state['features/base/responsive-ui'].aspectRatio,
266
+        _clientHeight: responsiveUI.clientHeight,
267
+        _clientWidth: responsiveUI.clientWidth,
268
+        _localParticipantId: getLocalParticipant(state)?.id,
177
         _participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
269
         _participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
178
         _visible: enabled && isFilmstripVisible(state)
270
         _visible: enabled && isFilmstripVisible(state)
179
     };
271
     };

+ 182
- 114
react/features/filmstrip/components/native/Thumbnail.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { useCallback } from 'react';
3
+import React, { PureComponent } from 'react';
4
 import { View } from 'react-native';
4
 import { View } from 'react-native';
5
 import type { Dispatch } from 'redux';
5
 import type { Dispatch } from 'redux';
6
 
6
 
25
 import { toggleToolboxVisible } from '../../../toolbox/actions.native';
25
 import { toggleToolboxVisible } from '../../../toolbox/actions.native';
26
 import { RemoteVideoMenu } from '../../../video-menu';
26
 import { RemoteVideoMenu } from '../../../video-menu';
27
 import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
27
 import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
28
-import SharedVideoMenu
29
-    from '../../../video-menu/components/native/SharedVideoMenu';
28
+import SharedVideoMenu from '../../../video-menu/components/native/SharedVideoMenu';
29
+import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
30
 
30
 
31
 import AudioMutedIndicator from './AudioMutedIndicator';
31
 import AudioMutedIndicator from './AudioMutedIndicator';
32
 import DominantSpeakerIndicator from './DominantSpeakerIndicator';
32
 import DominantSpeakerIndicator from './DominantSpeakerIndicator';
47
     _audioMuted: boolean,
47
     _audioMuted: boolean,
48
 
48
 
49
     /**
49
     /**
50
-     * The Redux representation of the state "features/large-video".
50
+     * Indicates whether the participant is fake.
51
      */
51
      */
52
-    _largeVideo: Object,
52
+    _isFakeParticipant: boolean,
53
+
54
+    /**
55
+     * Indicates whether the participant is fake.
56
+     */
57
+    _isScreenShare: boolean,
58
+
59
+    /**
60
+     * Indicates whether the participant is local.
61
+     */
62
+    _local: boolean,
53
 
63
 
54
     /**
64
     /**
55
      * Shared video local participant owner.
65
      * Shared video local participant owner.
57
     _localVideoOwner: boolean,
67
     _localVideoOwner: boolean,
58
 
68
 
59
     /**
69
     /**
60
-     * The Redux representation of the participant to display.
70
+     * The ID of the participant obtain from the participant object in Redux.
71
+     *
72
+     * NOTE: Generally it should be the same as the participantID prop except the case where the passed
73
+     * participantID doesn't corespond to any of the existing participants.
74
+     */
75
+    _participantId: string,
76
+
77
+    /**
78
+     * Indicates whether the participant is displayed on the large video.
79
+     */
80
+    _participantInLargeVideo: boolean,
81
+
82
+    /**
83
+     * Indicates whether the participant is pinned or not.
61
      */
84
      */
62
-     _participant: Object,
85
+    _pinned: boolean,
63
 
86
 
64
     /**
87
     /**
65
      * Whether to show the dominant speaker indicator or not.
88
      * Whether to show the dominant speaker indicator or not.
77
     _styles: StyleType,
100
     _styles: StyleType,
78
 
101
 
79
     /**
102
     /**
80
-     * The Redux representation of the participant's video track.
103
+     * Indicates whether the participant is video muted.
81
      */
104
      */
82
-    _videoTrack: Object,
105
+    _videoMuted: boolean,
83
 
106
 
84
     /**
107
     /**
85
      * If true, there will be no color overlay (tint) on the thumbnail
108
      * If true, there will be no color overlay (tint) on the thumbnail
93
      */
116
      */
94
     dispatch: Dispatch<any>,
117
     dispatch: Dispatch<any>,
95
 
118
 
119
+    /**
120
+     * The height of the thumnail.
121
+     */
122
+    height: ?number,
123
+
96
     /**
124
     /**
97
      * The ID of the participant related to the thumbnail.
125
      * The ID of the participant related to the thumbnail.
98
      */
126
      */
103
      */
131
      */
104
     renderDisplayName: ?boolean,
132
     renderDisplayName: ?boolean,
105
 
133
 
106
-    /**
107
-     * Optional styling to add or override on the Thumbnail component root.
108
-     */
109
-    styleOverrides?: Object,
110
-
111
     /**
134
     /**
112
      * If true, it tells the thumbnail that it needs to behave differently. E.g. react differently to a single tap.
135
      * If true, it tells the thumbnail that it needs to behave differently. E.g. react differently to a single tap.
113
      */
136
      */
116
 
139
 
117
 /**
140
 /**
118
  * React component for video thumbnail.
141
  * React component for video thumbnail.
119
- *
120
- * @param {Props} props - Properties passed to this functional component.
121
- * @returns {Component} - A React component.
122
  */
142
  */
123
-function Thumbnail(props: Props) {
124
-    const {
125
-        _audioMuted: audioMuted,
126
-        _largeVideo: largeVideo,
127
-        _localVideoOwner,
128
-        _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
129
-        _renderModeratorIndicator: renderModeratorIndicator,
130
-        _participant: participant,
131
-        _styles,
132
-        _videoTrack: videoTrack,
133
-        dispatch,
134
-        disableTint,
135
-        renderDisplayName,
136
-        tileView
137
-    } = props;
138
-
139
-    // It seems that on leave the Thumbnail for the left participant can be re-rendered.
140
-    // This will happen when mapStateToProps is executed before the remoteParticipants list in redux is updated.
141
-    if (typeof participant === 'undefined') {
142
-
143
-        return null;
143
+class Thumbnail extends PureComponent<Props> {
144
+
145
+    /**
146
+     * Creates new Thumbnail component.
147
+     *
148
+     * @param {Props} props - The props of the component.
149
+     * @returns {Thumbnail}
150
+     */
151
+    constructor(props: Props) {
152
+        super(props);
153
+
154
+        this._onClick = this._onClick.bind(this);
155
+        this._onThumbnailLongPress = this._onThumbnailLongPress.bind(this);
144
     }
156
     }
145
 
157
 
146
-    const participantId = participant.id;
147
-    const participantInLargeVideo
148
-        = participantId === largeVideo.participantId;
149
-    const videoMuted = !videoTrack || videoTrack.muted;
150
-    const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
151
-    const onClick = useCallback(() => {
158
+    _onClick: () => void;
159
+
160
+    /**
161
+     * Thumbnail click handler.
162
+     *
163
+     * @returns {void}
164
+     */
165
+    _onClick() {
166
+        const { _participantId, _pinned, dispatch, tileView } = this.props;
167
+
152
         if (tileView) {
168
         if (tileView) {
153
             dispatch(toggleToolboxVisible());
169
             dispatch(toggleToolboxVisible());
154
         } else {
170
         } else {
155
-            dispatch(pinParticipant(participant.pinned ? null : participant.id));
171
+            dispatch(pinParticipant(_pinned ? null : _participantId));
156
         }
172
         }
157
-    }, [ participant, tileView, dispatch ]);
158
-    const onThumbnailLongPress = useCallback(() => {
159
-        if (participant.local) {
173
+    }
174
+
175
+    _onThumbnailLongPress: () => void;
176
+
177
+    /**
178
+     * Thumbnail long press handler.
179
+     *
180
+     * @returns {void}
181
+     */
182
+    _onThumbnailLongPress() {
183
+        const { _participantId, _local, _isFakeParticipant, _localVideoOwner, dispatch } = this.props;
184
+
185
+        if (_local) {
160
             dispatch(openDialog(ConnectionStatusComponent, {
186
             dispatch(openDialog(ConnectionStatusComponent, {
161
-                participantID: participant.id
187
+                participantID: _participantId
162
             }));
188
             }));
163
-        } else if (participant.isFakeParticipant) {
189
+        } else if (_isFakeParticipant) {
164
             if (_localVideoOwner) {
190
             if (_localVideoOwner) {
165
                 dispatch(openDialog(SharedVideoMenu, {
191
                 dispatch(openDialog(SharedVideoMenu, {
166
-                    participant
192
+                    _participantId
167
                 }));
193
                 }));
168
             }
194
             }
169
         } else {
195
         } else {
170
             dispatch(openDialog(RemoteVideoMenu, {
196
             dispatch(openDialog(RemoteVideoMenu, {
171
-                participant
197
+                participantId: _participantId
172
             }));
198
             }));
173
         }
199
         }
174
-    }, [ participant, dispatch ]);
175
-
176
-    return (
177
-        <Container
178
-            onClick = { onClick }
179
-            onLongPress = { onThumbnailLongPress }
180
-            style = { [
181
-                styles.thumbnail,
182
-                participant.pinned && !tileView
183
-                    ? _styles.thumbnailPinned : null,
184
-                props.styleOverrides || null
185
-            ] }
186
-            touchFeedback = { false }>
187
-
188
-            <ParticipantView
189
-                avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
190
-                disableVideo = { isScreenShare || participant.isFakeParticipant }
191
-                participantId = { participantId }
192
-                style = { _styles.participantViewStyle }
193
-                tintEnabled = { participantInLargeVideo && !disableTint }
194
-                tintStyle = { _styles.activeThumbnailTint }
195
-                zOrder = { 1 } />
196
-
197
-            { renderDisplayName && <Container style = { styles.displayNameContainer }>
198
-                <DisplayNameLabel participantId = { participantId } />
199
-            </Container> }
200
-
201
-            { renderModeratorIndicator
202
-                && <View style = { styles.moderatorIndicatorContainer }>
203
-                    <ModeratorIndicator />
204
-                </View>}
205
-
206
-            { !participant.isFakeParticipant && <View
207
-                style = { [
208
-                    styles.thumbnailTopIndicatorContainer,
209
-                    styles.thumbnailTopLeftIndicatorContainer
210
-                ] }>
211
-                <RaisedHandIndicator participantId = { participant.id } />
212
-                { renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
213
-            </View> }
214
-
215
-            { !participant.isFakeParticipant && <View
200
+    }
201
+
202
+    /**
203
+     * Implements React's {@link Component#render()}.
204
+     *
205
+     * @inheritdoc
206
+     * @returns {ReactElement}
207
+     */
208
+    render() {
209
+        const {
210
+            _audioMuted: audioMuted,
211
+            _isScreenShare: isScreenShare,
212
+            _isFakeParticipant,
213
+            _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
214
+            _renderModeratorIndicator: renderModeratorIndicator,
215
+            _participantId: participantId,
216
+            _participantInLargeVideo: participantInLargeVideo,
217
+            _pinned,
218
+            _styles,
219
+            _videoMuted: videoMuted,
220
+            disableTint,
221
+            height,
222
+            renderDisplayName,
223
+            tileView
224
+        } = this.props;
225
+        const styleOverrides = tileView ? {
226
+            aspectRatio: SQUARE_TILE_ASPECT_RATIO,
227
+            flex: 0,
228
+            height,
229
+            maxHeight: null,
230
+            maxWidth: null,
231
+            width: null
232
+        } : null;
233
+
234
+        return (
235
+            <Container
236
+                onClick = { this._onClick }
237
+                onLongPress = { this._onThumbnailLongPress }
216
                 style = { [
238
                 style = { [
217
-                    styles.thumbnailTopIndicatorContainer,
218
-                    styles.thumbnailTopRightIndicatorContainer
219
-                ] }>
220
-                <ConnectionIndicator participantId = { participant.id } />
221
-            </View> }
222
-
223
-            { !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
224
-                { audioMuted
225
-                    && <AudioMutedIndicator /> }
226
-                { videoMuted
227
-                    && <VideoMutedIndicator /> }
228
-                { isScreenShare
229
-                    && <ScreenShareIndicator /> }
230
-            </Container> }
231
-
232
-        </Container>
233
-    );
239
+                    styles.thumbnail,
240
+                    _pinned && !tileView ? _styles.thumbnailPinned : null,
241
+                    styleOverrides
242
+                ] }
243
+                touchFeedback = { false }>
244
+                <ParticipantView
245
+                    avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
246
+                    disableVideo = { isScreenShare || _isFakeParticipant }
247
+                    participantId = { participantId }
248
+                    style = { _styles.participantViewStyle }
249
+                    tintEnabled = { participantInLargeVideo && !disableTint }
250
+                    tintStyle = { _styles.activeThumbnailTint }
251
+                    zOrder = { 1 } />
252
+                {
253
+                    renderDisplayName
254
+                        && <Container style = { styles.displayNameContainer }>
255
+                            <DisplayNameLabel participantId = { participantId } />
256
+                        </Container>
257
+                }
258
+                { renderModeratorIndicator
259
+                    && <View style = { styles.moderatorIndicatorContainer }>
260
+                        <ModeratorIndicator />
261
+                    </View>
262
+                }
263
+                {
264
+                    !_isFakeParticipant
265
+                        && <View
266
+                            style = { [
267
+                                styles.thumbnailTopIndicatorContainer,
268
+                                styles.thumbnailTopLeftIndicatorContainer
269
+                            ] }>
270
+                            <RaisedHandIndicator participantId = { participantId } />
271
+                            { renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
272
+                        </View>
273
+                }
274
+                {
275
+                    !_isFakeParticipant
276
+                        && <View
277
+                            style = { [
278
+                                styles.thumbnailTopIndicatorContainer,
279
+                                styles.thumbnailTopRightIndicatorContainer
280
+                            ] }>
281
+                            <ConnectionIndicator participantId = { participantId } />
282
+                        </View>
283
+                }
284
+                {
285
+                    !_isFakeParticipant
286
+                        && <Container style = { styles.thumbnailIndicatorContainer }>
287
+                            { audioMuted && <AudioMutedIndicator /> }
288
+                            { videoMuted && <VideoMutedIndicator /> }
289
+                            { isScreenShare && <ScreenShareIndicator /> }
290
+                        </Container>
291
+                }
292
+            </Container>
293
+        );
294
+    }
234
 }
295
 }
235
 
296
 
236
 /**
297
 /**
255
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
316
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
256
     const videoTrack
317
     const videoTrack
257
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
318
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
319
+    const videoMuted = !videoTrack || videoTrack.muted;
320
+    const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
258
     const participantCount = getParticipantCount(state);
321
     const participantCount = getParticipantCount(state);
259
     const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
322
     const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
260
     const _isEveryoneModerator = isEveryoneModerator(state);
323
     const _isEveryoneModerator = isEveryoneModerator(state);
261
     const renderModeratorIndicator = !_isEveryoneModerator
324
     const renderModeratorIndicator = !_isEveryoneModerator
262
         && participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
325
         && participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
326
+    const participantInLargeVideo = id === largeVideo.participantId;
263
 
327
 
264
     return {
328
     return {
265
         _audioMuted: audioTrack?.muted ?? true,
329
         _audioMuted: audioTrack?.muted ?? true,
266
-        _largeVideo: largeVideo,
330
+        _isScreenShare: isScreenShare,
331
+        _isFakeParticipant: participant?.isFakeParticipant,
332
+        _local: participant?.local,
267
         _localVideoOwner: Boolean(ownerId === localParticipantId),
333
         _localVideoOwner: Boolean(ownerId === localParticipantId),
268
-        _participant: participant,
334
+        _participantInLargeVideo: participantInLargeVideo,
335
+        _participantId: id,
336
+        _pinned: participant?.pinned,
269
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
337
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
270
         _renderModeratorIndicator: renderModeratorIndicator,
338
         _renderModeratorIndicator: renderModeratorIndicator,
271
         _styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
339
         _styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
272
-        _videoTrack: videoTrack
340
+        _videoMuted: videoMuted
273
     };
341
     };
274
 }
342
 }
275
 
343
 

+ 132
- 170
react/features/filmstrip/components/native/TileView.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { Component } from 'react';
3
+import React, { PureComponent } from 'react';
4
 import {
4
 import {
5
-    ScrollView,
5
+    FlatList,
6
     TouchableWithoutFeedback,
6
     TouchableWithoutFeedback,
7
     View
7
     View
8
 } from 'react-native';
8
 } from 'react-native';
10
 
10
 
11
 import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
11
 import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
12
 import { connect } from '../../../base/redux';
12
 import { connect } from '../../../base/redux';
13
-import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
14
-import { setTileViewDimensions } from '../../actions.native';
13
+import { setVisibleRemoteParticipants } from '../../actions.web';
15
 
14
 
16
 import Thumbnail from './Thumbnail';
15
 import Thumbnail from './Thumbnail';
17
 import styles from './styles';
16
 import styles from './styles';
18
 
17
 
19
-
20
 /**
18
 /**
21
  * The type of the React {@link Component} props of {@link TileView}.
19
  * The type of the React {@link Component} props of {@link TileView}.
22
  */
20
  */
27
      */
25
      */
28
     _aspectRatio: Symbol,
26
     _aspectRatio: Symbol,
29
 
27
 
28
+    /**
29
+     * The number of columns.
30
+     */
31
+    _columns: number,
32
+
30
     /**
33
     /**
31
      * Application's viewport height.
34
      * Application's viewport height.
32
      */
35
      */
47
      */
50
      */
48
     _remoteParticipants: Array<string>,
51
     _remoteParticipants: Array<string>,
49
 
52
 
53
+    /**
54
+     * The thumbnail height.
55
+     */
56
+    _thumbnailHeight: number,
57
+
50
     /**
58
     /**
51
      * Application's viewport height.
59
      * Application's viewport height.
52
      */
60
      */
64
 };
72
 };
65
 
73
 
66
 /**
74
 /**
67
- * The margin for each side of the tile view. Taken away from the available
68
- * height and width for the tile container to display in.
75
+ * Implements a React {@link PureComponent} which displays thumbnails in a two
76
+ * dimensional grid.
69
  *
77
  *
70
- * @private
71
- * @type {number}
78
+ * @extends PureComponent
72
  */
79
  */
73
-const MARGIN = 10;
80
+class TileView extends PureComponent<Props> {
74
 
81
 
75
-/**
76
- * The aspect ratio the tiles should display in.
77
- *
78
- * @private
79
- * @type {number}
80
- */
81
-const TILE_ASPECT_RATIO = 1;
82
+    /**
83
+     * The FlatList's viewabilityConfig.
84
+     */
85
+    _viewabilityConfig: Object;
82
 
86
 
83
-/**
84
- * Implements a React {@link Component} which displays thumbnails in a two
85
- * dimensional grid.
86
- *
87
- * @extends Component
88
- */
89
-class TileView extends Component<Props> {
90
     /**
87
     /**
91
-     * Implements React's {@link Component#componentDidMount}.
88
+     * The styles for the FlatList.
89
+     */
90
+    _flatListStyles: Object;
91
+
92
+    /**
93
+     * The styles for the content container of the FlatList.
94
+     */
95
+    _contentContainerStyles: Object;
96
+
97
+    /**
98
+     * Creates new TileView component.
92
      *
99
      *
93
-     * @inheritdoc
100
+     * @param {Props} props - The props of the component.
94
      */
101
      */
95
-    componentDidMount() {
96
-        this._updateReceiverQuality();
102
+    constructor(props: Props) {
103
+        super(props);
104
+
105
+        this._keyExtractor = this._keyExtractor.bind(this);
106
+        this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
107
+        this._renderThumbnail = this._renderThumbnail.bind(this);
108
+        this._viewabilityConfig = {
109
+            itemVisiblePercentThreshold: 30
110
+        };
111
+        this._flatListStyles = {
112
+            ...styles.flatList
113
+        };
114
+        this._contentContainerStyles = {
115
+            ...styles.contentContainer
116
+        };
97
     }
117
     }
98
 
118
 
119
+    _keyExtractor: string => string;
120
+
99
     /**
121
     /**
100
-     * Implements React's {@link Component#componentDidUpdate}.
122
+     * Returns a key for a passed item of the list.
101
      *
123
      *
102
-     * @inheritdoc
124
+     * @param {string} item - The user ID.
125
+     * @returns {string} - The user ID.
103
      */
126
      */
104
-    componentDidUpdate() {
105
-        this._updateReceiverQuality();
127
+    _keyExtractor(item) {
128
+        return item;
106
     }
129
     }
107
 
130
 
131
+    _onViewableItemsChanged: Object => void;
132
+
108
     /**
133
     /**
109
-     * Implements React's {@link Component#render()}.
134
+     * A handler for visible items changes.
110
      *
135
      *
111
-     * @inheritdoc
112
-     * @returns {ReactElement}
136
+     * @param {Object} data - The visible items data.
137
+     * @param {Array<Object>} data.viewableItems - The visible items array.
138
+     * @returns {void}
113
      */
139
      */
114
-    render() {
115
-        const { _height, _width, onClick } = this.props;
116
-        const rowElements = this._groupIntoRows(this._renderThumbnails(), this._getColumnCount());
140
+    _onViewableItemsChanged({ viewableItems = [] }: { viewableItems: Array<Object> }) {
141
+        const indexArray = viewableItems.map(i => i.index);
117
 
142
 
118
-        return (
119
-            <ScrollView
120
-                style = {{
121
-                    ...styles.tileView,
122
-                    height: _height,
123
-                    width: _width
124
-                }}>
125
-                <TouchableWithoutFeedback onPress = { onClick }>
126
-                    <View
127
-                        style = {{
128
-                            ...styles.tileViewRows,
129
-                            minHeight: _height,
130
-                            minWidth: _width
131
-                        }}>
132
-                        { rowElements }
133
-                    </View>
134
-                </TouchableWithoutFeedback>
135
-            </ScrollView>
136
-        );
143
+        // We need to shift the start index of the remoteParticipants array with 1 because of the local video placed
144
+        // at the beginning and in the same time we don't need to adjust the end index because the end index will not be
145
+        // included.
146
+        const startIndex = Math.max(Math.min(...indexArray) - 1, 0);
147
+        const endIndex = Math.max(...indexArray);
148
+
149
+        this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
137
     }
150
     }
138
 
151
 
139
     /**
152
     /**
140
-     * Returns how many columns should be displayed for tile view.
153
+     * Implements React's {@link Component#render()}.
141
      *
154
      *
142
-     * @returns {number}
143
-     * @private
155
+     * @inheritdoc
156
+     * @returns {ReactElement}
144
      */
157
      */
145
-    _getColumnCount() {
146
-        const participantCount = this.props._participantCount;
147
-
148
-        // For narrow view, tiles should stack on top of each other for a lonely
149
-        // call and a 1:1 call. Otherwise tiles should be grouped into rows of
150
-        // two.
151
-        if (this.props._aspectRatio === ASPECT_RATIO_NARROW) {
152
-            return participantCount >= 3 ? 2 : 1;
158
+    render() {
159
+        const { _columns, _height, _thumbnailHeight, _width, onClick } = this.props;
160
+        const participants = this._getSortedParticipants();
161
+        const initialRowsToRender = Math.ceil(_height / (_thumbnailHeight + (2 * styles.thumbnail.margin)));
162
+
163
+        if (this._flatListStyles.minHeight !== _height || this._flatListStyles.minWidth !== _width) {
164
+            this._flatListStyles = {
165
+                ...styles.flatList,
166
+                minHeight: _height,
167
+                minWidth: _width
168
+            };
153
         }
169
         }
154
 
170
 
155
-        if (participantCount === 4) {
156
-            // In wide view, a four person call should display as a 2x2 grid.
157
-            return 2;
171
+        if (this._contentContainerStyles.minHeight !== _height || this._contentContainerStyles.minWidth !== _width) {
172
+            this._contentContainerStyles = {
173
+                ...styles.contentContainer,
174
+                minHeight: _height,
175
+                minWidth: _width
176
+            };
158
         }
177
         }
159
 
178
 
160
-        return Math.min(3, participantCount);
179
+        return (
180
+            <TouchableWithoutFeedback onPress = { onClick }>
181
+                <View style = { styles.flatListContainer }>
182
+                    <FlatList
183
+                        contentContainerStyle = { this._contentContainerStyles }
184
+                        data = { participants }
185
+                        horizontal = { false }
186
+                        initialNumToRender = { initialRowsToRender }
187
+                        key = { _columns }
188
+                        keyExtractor = { this._keyExtractor }
189
+                        numColumns = { _columns }
190
+                        onViewableItemsChanged = { this._onViewableItemsChanged }
191
+                        renderItem = { this._renderThumbnail }
192
+                        showsHorizontalScrollIndicator = { false }
193
+                        showsVerticalScrollIndicator = { false }
194
+                        style = { this._flatListStyles }
195
+                        viewabilityConfig = { this._viewabilityConfig }
196
+                        windowSize = { 2 } />
197
+                </View>
198
+            </TouchableWithoutFeedback>
199
+        );
161
     }
200
     }
162
 
201
 
163
     /**
202
     /**
168
      */
207
      */
169
     _getSortedParticipants() {
208
     _getSortedParticipants() {
170
         const { _localParticipant, _remoteParticipants } = this.props;
209
         const { _localParticipant, _remoteParticipants } = this.props;
171
-        const participants = [ ..._remoteParticipants ];
210
+        const participants = [];
172
 
211
 
173
         _localParticipant && participants.push(_localParticipant.id);
212
         _localParticipant && participants.push(_localParticipant.id);
174
 
213
 
175
-        return participants;
176
-    }
177
-
178
-    /**
179
-     * Calculate the height and width for the tiles.
180
-     *
181
-     * @private
182
-     * @returns {Object}
183
-     */
184
-    _getTileDimensions() {
185
-        const { _height, _participantCount, _width } = this.props;
186
-        const columns = this._getColumnCount();
187
-        const heightToUse = _height - (MARGIN * 2);
188
-        const widthToUse = _width - (MARGIN * 2);
189
-        let tileWidth;
190
-
191
-        // If there is going to be at least two rows, ensure that at least two
192
-        // rows display fully on screen.
193
-        if (_participantCount / columns > 1) {
194
-            tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
195
-        } else {
196
-            tileWidth = Math.min(widthToUse / columns, heightToUse);
197
-        }
198
-
199
-        return {
200
-            height: tileWidth / TILE_ASPECT_RATIO,
201
-            width: tileWidth
202
-        };
214
+        return [ ...participants, ..._remoteParticipants ];
203
     }
215
     }
204
 
216
 
205
-    /**
206
-     * Splits a list of thumbnails into React Elements with a maximum of
207
-     * {@link rowLength} thumbnails in each.
208
-     *
209
-     * @param {Array} thumbnails - The list of thumbnails that should be split
210
-     * into separate row groupings.
211
-     * @param {number} rowLength - How many thumbnails should be in each row.
212
-     * @private
213
-     * @returns {ReactElement[]}
214
-     */
215
-    _groupIntoRows(thumbnails, rowLength) {
216
-        const rowElements = [];
217
-
218
-        for (let i = 0; i < thumbnails.length; i++) {
219
-            if (i % rowLength === 0) {
220
-                const thumbnailsInRow = thumbnails.slice(i, i + rowLength);
221
-
222
-                rowElements.push(
223
-                    <View
224
-                        key = { rowElements.length }
225
-                        style = { styles.tileViewRow }>
226
-                        { thumbnailsInRow }
227
-                    </View>
228
-                );
229
-            }
230
-        }
231
-
232
-        return rowElements;
233
-    }
217
+    _renderThumbnail: Object => Object;
234
 
218
 
235
     /**
219
     /**
236
-     * Creates React Elements to display each participant in a thumbnail. Each
237
-     * tile will be.
220
+     * Creates React Element to display each participant in a thumbnail.
238
      *
221
      *
239
      * @private
222
      * @private
240
-     * @returns {ReactElement[]}
223
+     * @returns {ReactElement}
241
      */
224
      */
242
-    _renderThumbnails() {
243
-        const styleOverrides = {
244
-            aspectRatio: TILE_ASPECT_RATIO,
245
-            flex: 0,
246
-            height: this._getTileDimensions().height,
247
-            maxHeight: null,
248
-            maxWidth: null,
249
-            width: null
250
-        };
251
-
252
-        return this._getSortedParticipants()
253
-            .map(id => (
254
-                <Thumbnail
255
-                    disableTint = { true }
256
-                    key = { id }
257
-                    participantID = { id }
258
-                    renderDisplayName = { true }
259
-                    styleOverrides = { styleOverrides }
260
-                    tileView = { true } />));
261
-    }
225
+    _renderThumbnail({ item/* , index , separators */ }) {
226
+        const { _thumbnailHeight } = this.props;
262
 
227
 
263
-    /**
264
-     * Sets the receiver video quality based on the dimensions of the thumbnails
265
-     * that are displayed.
266
-     *
267
-     * @private
268
-     * @returns {void}
269
-     */
270
-    _updateReceiverQuality() {
271
-        const { height, width } = this._getTileDimensions();
272
-
273
-        this.props.dispatch(setTileViewDimensions({
274
-            thumbnailSize: {
275
-                height,
276
-                width
277
-            }
278
-        }));
228
+        return (
229
+            <Thumbnail
230
+                disableTint = { true }
231
+                height = { _thumbnailHeight }
232
+                key = { item }
233
+                participantID = { item }
234
+                renderDisplayName = { true }
235
+                tileView = { true } />)
236
+        ;
279
     }
237
     }
280
 }
238
 }
281
 
239
 
288
  */
246
  */
289
 function _mapStateToProps(state) {
247
 function _mapStateToProps(state) {
290
     const responsiveUi = state['features/base/responsive-ui'];
248
     const responsiveUi = state['features/base/responsive-ui'];
291
-    const { remoteParticipants } = state['features/filmstrip'];
249
+    const { remoteParticipants, tileViewDimensions } = state['features/filmstrip'];
250
+    const { height } = tileViewDimensions.thumbnailSize;
251
+    const { columns } = tileViewDimensions;
292
 
252
 
293
     return {
253
     return {
294
         _aspectRatio: responsiveUi.aspectRatio,
254
         _aspectRatio: responsiveUi.aspectRatio,
255
+        _columns: columns,
295
         _height: responsiveUi.clientHeight,
256
         _height: responsiveUi.clientHeight,
296
         _localParticipant: getLocalParticipant(state),
257
         _localParticipant: getLocalParticipant(state),
297
         _participantCount: getParticipantCountWithFake(state),
258
         _participantCount: getParticipantCountWithFake(state),
298
         _remoteParticipants: remoteParticipants,
259
         _remoteParticipants: remoteParticipants,
260
+        _thumbnailHeight: height,
299
         _width: responsiveUi.clientWidth
261
         _width: responsiveUi.clientWidth
300
     };
262
     };
301
 }
263
 }

+ 25
- 13
react/features/filmstrip/components/native/styles.js View File

14
  */
14
  */
15
 export default {
15
 export default {
16
 
16
 
17
+    /**
18
+     * The FlatList content container styles
19
+     */
20
+    contentContainer: {
21
+        alignItems: 'center',
22
+        justifyContent: 'center',
23
+        flex: 0
24
+    },
25
+
17
     /**
26
     /**
18
      * The display name container.
27
      * The display name container.
19
      */
28
      */
52
         top: 0
61
         top: 0
53
     },
62
     },
54
 
63
 
64
+    /**
65
+     * The styles for the FlatList container.
66
+     */
67
+    flatListContainer: {
68
+        flexGrow: 1,
69
+        flexShrink: 1,
70
+        flex: 0
71
+    },
72
+
73
+    /**
74
+     * The styles for the FlatList.
75
+     */
76
+    flatList: {
77
+        flex: 0
78
+    },
79
+
55
     /**
80
     /**
56
      * Container of the {@link LocalThumbnail}.
81
      * Container of the {@link LocalThumbnail}.
57
      */
82
      */
122
 
147
 
123
     thumbnailTopRightIndicatorContainer: {
148
     thumbnailTopRightIndicatorContainer: {
124
         right: 0
149
         right: 0
125
-    },
126
-
127
-    tileView: {
128
-        alignSelf: 'center'
129
-    },
130
-
131
-    tileViewRows: {
132
-        justifyContent: 'center'
133
-    },
134
-
135
-    tileViewRow: {
136
-        flexDirection: 'row',
137
-        justifyContent: 'center'
138
     }
150
     }
139
 };
151
 };
140
 
152
 

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

15
     /**
15
     /**
16
      * The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
16
      * The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
17
      */
17
      */
18
-     _horizontalOffset: number,
18
+    _horizontalOffset: number,
19
 
19
 
20
     /**
20
     /**
21
      * The ID of the participant associated with the Thumbnail.
21
      * The ID of the participant associated with the Thumbnail.

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

220
  * @type {number}
220
  * @type {number}
221
  */
221
  */
222
 export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
222
 export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
223
+
224
+/**
225
+ * The margin for each side of the tile view. Taken away from the available
226
+ * height and width for the tile container to display in.
227
+ *
228
+ * NOTE: Mobile specific.
229
+ *
230
+ * @private
231
+ * @type {number}
232
+ */
233
+export const TILE_MARGIN = 10;

+ 29
- 0
react/features/filmstrip/functions.native.js View File

3
 import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
3
 import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
4
 import { getParticipantCountWithFake, getPinnedParticipant } from '../base/participants';
4
 import { getParticipantCountWithFake, getPinnedParticipant } from '../base/participants';
5
 import { toState } from '../base/redux';
5
 import { toState } from '../base/redux';
6
+import { ASPECT_RATIO_NARROW } from '../base/responsive-ui/constants';
6
 
7
 
7
 export * from './functions.any';
8
 export * from './functions.any';
8
 
9
 
59
 
60
 
60
             || disable1On1Mode);
61
             || disable1On1Mode);
61
 }
62
 }
63
+
64
+/**
65
+ * Returns how many columns should be displayed for tile view.
66
+ *
67
+ * @param {Object | Function} stateful - The Object or Function that can be
68
+ * resolved to a Redux state object with the toState function.
69
+ * @returns {number} - The number of columns to be rendered in tile view.
70
+ * @private
71
+ */
72
+export function getColumnCount(stateful: Object | Function) {
73
+    const state = toState(stateful);
74
+    const participantCount = getParticipantCountWithFake(state);
75
+    const { aspectRatio } = state['features/base/responsive-ui'];
76
+
77
+    // For narrow view, tiles should stack on top of each other for a lonely
78
+    // call and a 1:1 call. Otherwise tiles should be grouped into rows of
79
+    // two.
80
+    if (aspectRatio === ASPECT_RATIO_NARROW) {
81
+        return participantCount >= 3 ? 2 : 1;
82
+    }
83
+
84
+    if (participantCount === 4) {
85
+        // In wide view, a four person call should display as a 2x2 grid.
86
+        return 2;
87
+    }
88
+
89
+    return Math.min(3, participantCount);
90
+}

+ 10
- 5
react/features/filmstrip/middleware.native.js View File

2
 
2
 
3
 import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
3
 import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
4
 import { MiddlewareRegistry } from '../base/redux';
4
 import { MiddlewareRegistry } from '../base/redux';
5
+import { CLIENT_RESIZED, SET_ASPECT_RATIO } from '../base/responsive-ui';
5
 
6
 
7
+import { setTileViewDimensions } from './actions';
6
 import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
8
 import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
7
 import './subscriber';
9
 import './subscriber';
8
 
10
 
10
  * The middleware of the feature Filmstrip.
12
  * The middleware of the feature Filmstrip.
11
  */
13
  */
12
 MiddlewareRegistry.register(store => next => action => {
14
 MiddlewareRegistry.register(store => next => action => {
15
+    if (action.type === PARTICIPANT_LEFT) {
16
+        updateRemoteParticipantsOnLeave(store, action.participant?.id);
17
+    }
18
+
13
     const result = next(action);
19
     const result = next(action);
14
 
20
 
15
     switch (action.type) {
21
     switch (action.type) {
22
+    case CLIENT_RESIZED:
23
+    case SET_ASPECT_RATIO:
24
+        store.dispatch(setTileViewDimensions());
25
+        break;
16
     case PARTICIPANT_JOINED: {
26
     case PARTICIPANT_JOINED: {
17
         updateRemoteParticipants(store);
27
         updateRemoteParticipants(store);
18
         break;
28
         break;
19
     }
29
     }
20
-    case PARTICIPANT_LEFT: {
21
-        updateRemoteParticipantsOnLeave(store, action.participant?.id);
22
-        break;
23
-    }
24
     }
30
     }
25
 
31
 
26
     return result;
32
     return result;
27
 });
33
 });
28
-

+ 5
- 7
react/features/filmstrip/reducer.js View File

118
             };
118
             };
119
         case SET_REMOTE_PARTICIPANTS: {
119
         case SET_REMOTE_PARTICIPANTS: {
120
             state.remoteParticipants = action.participants;
120
             state.remoteParticipants = action.participants;
121
+            const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
121
 
122
 
122
-            // TODO: implement this on mobile.
123
-            if (navigator.product !== 'ReactNative') {
124
-                const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
125
-
126
-                state.visibleRemoteParticipants = new Set(state.remoteParticipants.slice(startIndex, endIndex + 1));
127
-            }
123
+            state.visibleRemoteParticipants = new Set(state.remoteParticipants.slice(startIndex, endIndex + 1));
128
 
124
 
129
             return { ...state };
125
             return { ...state };
130
         }
126
         }
167
             }
163
             }
168
             delete state.participantsVolume[id];
164
             delete state.participantsVolume[id];
169
 
165
 
170
-            return state;
166
+            return {
167
+                ...state
168
+            };
171
         }
169
         }
172
         }
170
         }
173
 
171
 

+ 39
- 0
react/features/filmstrip/subscriber.native.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { getParticipantCountWithFake } from '../base/participants';
4
+import { StateListenerRegistry } from '../base/redux';
5
+import { getTileViewGridDimensions, shouldDisplayTileView } from '../video-layout';
6
+
7
+import { setTileViewDimensions } from './actions';
3
 import './subscriber.any';
8
 import './subscriber.any';
9
+
10
+/**
11
+ * Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
12
+ */
13
+StateListenerRegistry.register(
14
+    /* selector */ state => {
15
+        const participantCount = getParticipantCountWithFake(state);
16
+
17
+        if (participantCount < 5) { // the dimensions are updated only when the participant count is lower than 5.
18
+            return participantCount;
19
+        }
20
+
21
+        return 4; // make sure we don't update the dimensions.
22
+    },
23
+    /* listener */ (_, store) => {
24
+        const state = store.getState();
25
+
26
+        if (shouldDisplayTileView(state)) {
27
+            store.dispatch(setTileViewDimensions());
28
+        }
29
+    });
30
+
31
+/**
32
+ * Listens for changes in the selected layout to calculate the dimensions of the tile view grid and horizontal view.
33
+ */
34
+StateListenerRegistry.register(
35
+    /* selector */ state => shouldDisplayTileView(state),
36
+    /* listener */ (isTileView, store) => {
37
+        const state = store.getState();
38
+
39
+        if (isTileView) {
40
+            store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
41
+        }
42
+    });

+ 1
- 1
react/features/invite/functions.js View File

439
  */
439
  */
440
 export function isSipInviteEnabled(state: Object): boolean {
440
 export function isSipInviteEnabled(state: Object): boolean {
441
     const { sipInviteUrl } = state['features/base/config'];
441
     const { sipInviteUrl } = state['features/base/config'];
442
-    const { features = {} } = getLocalParticipant(state);
442
+    const { features = {} } = getLocalParticipant(state) || {};
443
 
443
 
444
     return state['features/base/jwt'].jwt
444
     return state['features/base/jwt'].jwt
445
         && Boolean(sipInviteUrl)
445
         && Boolean(sipInviteUrl)

+ 11
- 17
react/features/video-menu/components/native/RemoteVideoMenu.js View File

42
     dispatch: Function,
42
     dispatch: Function,
43
 
43
 
44
     /**
44
     /**
45
-     * The participant for which this menu opened for.
45
+     * The ID of the participant for which this menu opened for.
46
      */
46
      */
47
-    participant: Object,
47
+    participantId: String,
48
 
48
 
49
     /**
49
     /**
50
      * The color-schemed stylesheet of the BottomSheet.
50
      * The color-schemed stylesheet of the BottomSheet.
79
     /**
79
     /**
80
      * Display name of the participant retrieved from Redux.
80
      * Display name of the participant retrieved from Redux.
81
      */
81
      */
82
-    _participantDisplayName: string,
83
-
84
-    /**
85
-     * The ID of the participant.
86
-     */
87
-    _participantID: ?string,
82
+    _participantDisplayName: string
88
 }
83
 }
89
 
84
 
90
 // eslint-disable-next-line prefer-const
85
 // eslint-disable-next-line prefer-const
117
             _disableRemoteMute,
112
             _disableRemoteMute,
118
             _disableGrantModerator,
113
             _disableGrantModerator,
119
             _isParticipantAvailable,
114
             _isParticipantAvailable,
120
-            participant
115
+            participantId
121
         } = this.props;
116
         } = this.props;
122
         const buttonProps = {
117
         const buttonProps = {
123
             afterClick: this._onCancel,
118
             afterClick: this._onCancel,
124
             showLabel: true,
119
             showLabel: true,
125
-            participantID: participant.id,
120
+            participantID: participantId,
126
             styles: this.props._bottomSheetStyles.buttons
121
             styles: this.props._bottomSheetStyles.buttons
127
         };
122
         };
128
 
123
 
141
                 <PrivateMessageButton { ...buttonProps } />
136
                 <PrivateMessageButton { ...buttonProps } />
142
                 <ConnectionStatusButton { ...buttonProps } />
137
                 <ConnectionStatusButton { ...buttonProps } />
143
                 {/* <Divider style = { styles.divider } />*/}
138
                 {/* <Divider style = { styles.divider } />*/}
144
-                {/* <VolumeSlider participantID = { _participantID } />*/}
139
+                {/* <VolumeSlider participantID = { participantId } />*/}
145
             </BottomSheet>
140
             </BottomSheet>
146
         );
141
         );
147
     }
142
     }
172
      * @returns {React$Element}
167
      * @returns {React$Element}
173
      */
168
      */
174
     _renderMenuHeader() {
169
     _renderMenuHeader() {
175
-        const { _bottomSheetStyles, participant } = this.props;
170
+        const { _bottomSheetStyles, participantId } = this.props;
176
 
171
 
177
         return (
172
         return (
178
             <View
173
             <View
180
                     _bottomSheetStyles.sheet,
175
                     _bottomSheetStyles.sheet,
181
                     styles.participantNameContainer ] }>
176
                     styles.participantNameContainer ] }>
182
                 <Avatar
177
                 <Avatar
183
-                    participantId = { participant.id }
178
+                    participantId = { participantId }
184
                     size = { AVATAR_SIZE } />
179
                     size = { AVATAR_SIZE } />
185
                 <Text style = { styles.participantNameLabel }>
180
                 <Text style = { styles.participantNameLabel }>
186
                     { this.props._participantDisplayName }
181
                     { this.props._participantDisplayName }
200
  */
195
  */
201
 function _mapStateToProps(state, ownProps) {
196
 function _mapStateToProps(state, ownProps) {
202
     const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
197
     const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
203
-    const { participant } = ownProps;
198
+    const { participantId } = ownProps;
204
     const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
199
     const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
205
-    const isParticipantAvailable = getParticipantById(state, participant.id);
200
+    const isParticipantAvailable = getParticipantById(state, participantId);
206
     let { disableKick } = remoteVideoMenu;
201
     let { disableKick } = remoteVideoMenu;
207
 
202
 
208
     disableKick = disableKick || !kickOutEnabled;
203
     disableKick = disableKick || !kickOutEnabled;
213
         _disableRemoteMute: Boolean(disableRemoteMute),
208
         _disableRemoteMute: Boolean(disableRemoteMute),
214
         _isOpen: isDialogOpen(state, RemoteVideoMenu_),
209
         _isOpen: isDialogOpen(state, RemoteVideoMenu_),
215
         _isParticipantAvailable: Boolean(isParticipantAvailable),
210
         _isParticipantAvailable: Boolean(isParticipantAvailable),
216
-        _participantDisplayName: getParticipantDisplayName(state, participant.id),
217
-        _participantID: participant.id
211
+        _participantDisplayName: getParticipantDisplayName(state, participantId)
218
     };
212
     };
219
 }
213
 }
220
 
214
 

+ 9
- 15
react/features/video-menu/components/native/SharedVideoMenu.js View File

32
     dispatch: Function,
32
     dispatch: Function,
33
 
33
 
34
     /**
34
     /**
35
-     * The participant for which this menu opened for.
35
+     * The ID of the participant for which this menu opened for.
36
      */
36
      */
37
-    participant: Object,
37
+    participantId: string,
38
 
38
 
39
     /**
39
     /**
40
      * The color-schemed stylesheet of the BottomSheet.
40
      * The color-schemed stylesheet of the BottomSheet.
55
      * Display name of the participant retrieved from Redux.
55
      * Display name of the participant retrieved from Redux.
56
      */
56
      */
57
     _participantDisplayName: string,
57
     _participantDisplayName: string,
58
-
59
-    /**
60
-     * The ID of the participant.
61
-     */
62
-    _participantID: ?string,
63
 }
58
 }
64
 
59
 
65
 // eslint-disable-next-line prefer-const
60
 // eslint-disable-next-line prefer-const
89
     render() {
84
     render() {
90
         const {
85
         const {
91
             _isParticipantAvailable,
86
             _isParticipantAvailable,
92
-            participant
87
+            participantId
93
         } = this.props;
88
         } = this.props;
94
 
89
 
95
         const buttonProps = {
90
         const buttonProps = {
96
             afterClick: this._onCancel,
91
             afterClick: this._onCancel,
97
             showLabel: true,
92
             showLabel: true,
98
-            participantID: participant.id,
93
+            participantID: participantId,
99
             styles: this.props._bottomSheetStyles.buttons
94
             styles: this.props._bottomSheetStyles.buttons
100
         };
95
         };
101
 
96
 
136
      * @returns {React$Element}
131
      * @returns {React$Element}
137
      */
132
      */
138
     _renderMenuHeader() {
133
     _renderMenuHeader() {
139
-        const { _bottomSheetStyles, participant } = this.props;
134
+        const { _bottomSheetStyles, participantId } = this.props;
140
 
135
 
141
         return (
136
         return (
142
             <View
137
             <View
144
                     _bottomSheetStyles.sheet,
139
                     _bottomSheetStyles.sheet,
145
                     styles.participantNameContainer ] }>
140
                     styles.participantNameContainer ] }>
146
                 <Avatar
141
                 <Avatar
147
-                    participantId = { participant.id }
142
+                    participantId = { participantId }
148
                     size = { AVATAR_SIZE } />
143
                     size = { AVATAR_SIZE } />
149
                 <Text style = { styles.participantNameLabel }>
144
                 <Text style = { styles.participantNameLabel }>
150
                     { this.props._participantDisplayName }
145
                     { this.props._participantDisplayName }
163
  * @returns {Props}
158
  * @returns {Props}
164
  */
159
  */
165
 function _mapStateToProps(state, ownProps) {
160
 function _mapStateToProps(state, ownProps) {
166
-    const { participant } = ownProps;
167
-    const isParticipantAvailable = getParticipantById(state, participant.id);
161
+    const { participantId } = ownProps;
162
+    const isParticipantAvailable = getParticipantById(state, participantId);
168
 
163
 
169
     return {
164
     return {
170
         _bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
165
         _bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
171
         _isOpen: isDialogOpen(state, SharedVideoMenu_),
166
         _isOpen: isDialogOpen(state, SharedVideoMenu_),
172
         _isParticipantAvailable: Boolean(isParticipantAvailable),
167
         _isParticipantAvailable: Boolean(isParticipantAvailable),
173
-        _participantDisplayName: getParticipantDisplayName(state, participant.id),
174
-        _participantID: participant.id
168
+        _participantDisplayName: getParticipantDisplayName(state, participantId)
175
     };
169
     };
176
 }
170
 }
177
 
171
 

+ 1
- 6
react/features/video-quality/subscriber.js View File

191
     const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
191
     const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
192
     const { participantId: largeVideoParticipantId } = state['features/large-video'];
192
     const { participantId: largeVideoParticipantId } = state['features/large-video'];
193
     const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
193
     const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
194
-    let { visibleRemoteParticipants } = state['features/filmstrip'];
195
-
196
-    // TODO: implement this on mobile.
197
-    if (navigator.product === 'ReactNative') {
198
-        visibleRemoteParticipants = new Set(Array.from(state['features/base/participants'].remote.keys()));
199
-    }
194
+    const { visibleRemoteParticipants } = state['features/filmstrip'];
200
 
195
 
201
     const receiverConstraints = {
196
     const receiverConstraints = {
202
         constraints: {},
197
         constraints: {},

Loading…
Cancel
Save