Преглед на файлове

feat(filmstrip-pagination): mobile support.

master
Hristo Terezov преди 3 години
родител
ревизия
7dd43d93b6

+ 1
- 1
react/features/base/conference/middleware.any.js Целия файл

@@ -551,7 +551,7 @@ function _updateLocalParticipantInConference({ dispatch, getState }, next, actio
551 551
 
552 552
     const localParticipant = getLocalParticipant(getState);
553 553
 
554
-    if (conference && participant.id === localParticipant.id) {
554
+    if (conference && participant.id === localParticipant?.id) {
555 555
         if ('name' in participant) {
556 556
             conference.setDisplayName(participant.name);
557 557
         }

+ 4
- 3
react/features/base/responsive-ui/actions.js Целия файл

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

+ 26
- 1
react/features/filmstrip/actions.any.js Целия файл

@@ -1,6 +1,11 @@
1 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 11
  * Sets whether the filmstrip is enabled.
@@ -50,3 +55,23 @@ export function setRemoteParticipants(participants: Array<string>) {
50 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 Целия файл

@@ -1,6 +1,10 @@
1 1
 // @flow
2 2
 
3
+import { getParticipantCountWithFake } from '../base/participants';
4
+
3 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 9
 export * from './actions.any';
6 10
 
@@ -9,20 +13,40 @@ export * from './actions.any';
9 13
  * of the values are currently used. Check the description of {@link SET_TILE_VIEW_DIMENSIONS} for the full set
10 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 Целия файл

@@ -7,7 +7,6 @@ import {
7 7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
8 8
     SET_TILE_VIEW_DIMENSIONS,
9 9
     SET_VERTICAL_VIEW_DIMENSIONS,
10
-    SET_VISIBLE_REMOTE_PARTICIPANTS,
11 10
     SET_VOLUME
12 11
 } from './actionTypes';
13 12
 import {
@@ -159,23 +158,3 @@ export function setVolume(participantId: string, volume: number) {
159 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 Целия файл

@@ -1,11 +1,13 @@
1 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 7
 import { Platform } from '../../../base/react';
7 8
 import { connect } from '../../../base/redux';
8 9
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
10
+import { setVisibleRemoteParticipants } from '../../actions';
9 11
 import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
10 12
 
11 13
 import LocalThumbnail from './LocalThumbnail';
@@ -25,6 +27,12 @@ type Props = {
25 27
      */
26 28
     _aspectRatio: Symbol,
27 29
 
30
+    _clientWidth: number,
31
+
32
+    _clientHeight: number,
33
+
34
+    _localParticipantId: string,
35
+
28 36
     /**
29 37
      * The participants in the conference.
30 38
      */
@@ -33,7 +41,12 @@ type Props = {
33 41
     /**
34 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,13 +55,18 @@ type Props = {
42 55
  *
43 56
  * @extends Component
44 57
  */
45
-class Filmstrip extends Component<Props> {
58
+class Filmstrip extends PureComponent<Props> {
46 59
     /**
47 60
      * Whether the local participant should be rendered separately from the
48 61
      * remote participants i.e. outside of their {@link ScrollView}.
49 62
      */
50 63
     _separateLocalThumbnail: boolean;
51 64
 
65
+    /**
66
+     * The FlatList's viewabilityConfig.
67
+     */
68
+    _viewabilityConfig: Object;
69
+
52 70
     /**
53 71
      * Constructor of the component.
54 72
      *
@@ -75,6 +93,107 @@ class Filmstrip extends Component<Props> {
75 93
         // do not have much of a choice but to continue rendering LocalThumbnail
76 94
         // as any other remote Thumbnail on Android.
77 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,7 +203,7 @@ class Filmstrip extends Component<Props> {
84 203
      * @returns {ReactElement}
85 204
      */
86 205
     render() {
87
-        const { _aspectRatio, _participants, _visible } = this.props;
206
+        const { _aspectRatio, _localParticipantId, _participants, _visible } = this.props;
88 207
 
89 208
         if (!_visible) {
90 209
             return null;
@@ -92,6 +211,13 @@ class Filmstrip extends Component<Props> {
92 211
 
93 212
         const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
94 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 222
         return (
97 223
             <SafeAreaView style = { filmstripStyle }>
@@ -100,29 +226,20 @@ class Filmstrip extends Component<Props> {
100 226
                         && !isNarrowAspectRatio
101 227
                         && <LocalThumbnail />
102 228
                 }
103
-                <ScrollView
229
+                <FlatList
230
+                    data = { participants }
231
+                    getItemLayout = { this._getItemLayout }
104 232
                     horizontal = { isNarrowAspectRatio }
233
+                    initialNumToRender = { initialNumToRender }
234
+                    key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
235
+                    keyExtractor = { this._keyExtractor }
236
+                    onViewableItemsChanged = { this._onViewableItemsChanged }
237
+                    renderItem = { this._renderThumbnail }
105 238
                     showsHorizontalScrollIndicator = { false }
106 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 244
                     this._separateLocalThumbnail && isNarrowAspectRatio
128 245
                         && <LocalThumbnail />
@@ -130,35 +247,6 @@ class Filmstrip extends Component<Props> {
130 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,9 +259,13 @@ class Filmstrip extends Component<Props> {
171 259
 function _mapStateToProps(state) {
172 260
     const { enabled, remoteParticipants } = state['features/filmstrip'];
173 261
     const showRemoteVideos = shouldRemoteVideosBeVisible(state);
262
+    const responsiveUI = state['features/base/responsive-ui'];
174 263
 
175 264
     return {
176 265
         _aspectRatio: state['features/base/responsive-ui'].aspectRatio,
266
+        _clientHeight: responsiveUI.clientHeight,
267
+        _clientWidth: responsiveUI.clientWidth,
268
+        _localParticipantId: getLocalParticipant(state)?.id,
177 269
         _participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
178 270
         _visible: enabled && isFilmstripVisible(state)
179 271
     };

+ 182
- 114
react/features/filmstrip/components/native/Thumbnail.js Целия файл

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React, { useCallback } from 'react';
3
+import React, { PureComponent } from 'react';
4 4
 import { View } from 'react-native';
5 5
 import type { Dispatch } from 'redux';
6 6
 
@@ -25,8 +25,8 @@ import { DisplayNameLabel } from '../../../display-name';
25 25
 import { toggleToolboxVisible } from '../../../toolbox/actions.native';
26 26
 import { RemoteVideoMenu } from '../../../video-menu';
27 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 31
 import AudioMutedIndicator from './AudioMutedIndicator';
32 32
 import DominantSpeakerIndicator from './DominantSpeakerIndicator';
@@ -47,9 +47,19 @@ type Props = {
47 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 65
      * Shared video local participant owner.
@@ -57,9 +67,22 @@ type Props = {
57 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 88
      * Whether to show the dominant speaker indicator or not.
@@ -77,9 +100,9 @@ type Props = {
77 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 108
      * If true, there will be no color overlay (tint) on the thumbnail
@@ -93,6 +116,11 @@ type Props = {
93 116
      */
94 117
     dispatch: Dispatch<any>,
95 118
 
119
+    /**
120
+     * The height of the thumnail.
121
+     */
122
+    height: ?number,
123
+
96 124
     /**
97 125
      * The ID of the participant related to the thumbnail.
98 126
      */
@@ -103,11 +131,6 @@ type Props = {
103 131
      */
104 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 135
      * If true, it tells the thumbnail that it needs to behave differently. E.g. react differently to a single tap.
113 136
      */
@@ -116,121 +139,159 @@ type Props = {
116 139
 
117 140
 /**
118 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 168
         if (tileView) {
153 169
             dispatch(toggleToolboxVisible());
154 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 186
             dispatch(openDialog(ConnectionStatusComponent, {
161
-                participantID: participant.id
187
+                participantID: _participantId
162 188
             }));
163
-        } else if (participant.isFakeParticipant) {
189
+        } else if (_isFakeParticipant) {
164 190
             if (_localVideoOwner) {
165 191
                 dispatch(openDialog(SharedVideoMenu, {
166
-                    participant
192
+                    _participantId
167 193
                 }));
168 194
             }
169 195
         } else {
170 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 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,21 +316,28 @@ function _mapStateToProps(state, ownProps) {
255 316
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
256 317
     const videoTrack
257 318
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
319
+    const videoMuted = !videoTrack || videoTrack.muted;
320
+    const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
258 321
     const participantCount = getParticipantCount(state);
259 322
     const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
260 323
     const _isEveryoneModerator = isEveryoneModerator(state);
261 324
     const renderModeratorIndicator = !_isEveryoneModerator
262 325
         && participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
326
+    const participantInLargeVideo = id === largeVideo.participantId;
263 327
 
264 328
     return {
265 329
         _audioMuted: audioTrack?.muted ?? true,
266
-        _largeVideo: largeVideo,
330
+        _isScreenShare: isScreenShare,
331
+        _isFakeParticipant: participant?.isFakeParticipant,
332
+        _local: participant?.local,
267 333
         _localVideoOwner: Boolean(ownerId === localParticipantId),
268
-        _participant: participant,
334
+        _participantInLargeVideo: participantInLargeVideo,
335
+        _participantId: id,
336
+        _pinned: participant?.pinned,
269 337
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
270 338
         _renderModeratorIndicator: renderModeratorIndicator,
271 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 Целия файл

@@ -1,8 +1,8 @@
1 1
 // @flow
2 2
 
3
-import React, { Component } from 'react';
3
+import React, { PureComponent } from 'react';
4 4
 import {
5
-    ScrollView,
5
+    FlatList,
6 6
     TouchableWithoutFeedback,
7 7
     View
8 8
 } from 'react-native';
@@ -10,13 +10,11 @@ import type { Dispatch } from 'redux';
10 10
 
11 11
 import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
12 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 15
 import Thumbnail from './Thumbnail';
17 16
 import styles from './styles';
18 17
 
19
-
20 18
 /**
21 19
  * The type of the React {@link Component} props of {@link TileView}.
22 20
  */
@@ -27,6 +25,11 @@ type Props = {
27 25
      */
28 26
     _aspectRatio: Symbol,
29 27
 
28
+    /**
29
+     * The number of columns.
30
+     */
31
+    _columns: number,
32
+
30 33
     /**
31 34
      * Application's viewport height.
32 35
      */
@@ -47,6 +50,11 @@ type Props = {
47 50
      */
48 51
     _remoteParticipants: Array<string>,
49 52
 
53
+    /**
54
+     * The thumbnail height.
55
+     */
56
+    _thumbnailHeight: number,
57
+
50 58
     /**
51 59
      * Application's viewport height.
52 60
      */
@@ -64,100 +72,131 @@ type Props = {
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,114 +207,33 @@ class TileView extends Component<Props> {
168 207
      */
169 208
     _getSortedParticipants() {
170 209
         const { _localParticipant, _remoteParticipants } = this.props;
171
-        const participants = [ ..._remoteParticipants ];
210
+        const participants = [];
172 211
 
173 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 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,14 +246,18 @@ class TileView extends Component<Props> {
288 246
  */
289 247
 function _mapStateToProps(state) {
290 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 253
     return {
294 254
         _aspectRatio: responsiveUi.aspectRatio,
255
+        _columns: columns,
295 256
         _height: responsiveUi.clientHeight,
296 257
         _localParticipant: getLocalParticipant(state),
297 258
         _participantCount: getParticipantCountWithFake(state),
298 259
         _remoteParticipants: remoteParticipants,
260
+        _thumbnailHeight: height,
299 261
         _width: responsiveUi.clientWidth
300 262
     };
301 263
 }

+ 25
- 13
react/features/filmstrip/components/native/styles.js Целия файл

@@ -14,6 +14,15 @@ export const AVATAR_SIZE = 50;
14 14
  */
15 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 27
      * The display name container.
19 28
      */
@@ -52,6 +61,22 @@ export default {
52 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 81
      * Container of the {@link LocalThumbnail}.
57 82
      */
@@ -122,19 +147,6 @@ export default {
122 147
 
123 148
     thumbnailTopRightIndicatorContainer: {
124 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 Целия файл

@@ -15,7 +15,7 @@ type Props = {
15 15
     /**
16 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 21
      * The ID of the participant associated with the Thumbnail.

+ 11
- 0
react/features/filmstrip/constants.js Целия файл

@@ -220,3 +220,14 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
220 220
  * @type {number}
221 221
  */
222 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 Целия файл

@@ -3,6 +3,7 @@
3 3
 import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
4 4
 import { getParticipantCountWithFake, getPinnedParticipant } from '../base/participants';
5 5
 import { toState } from '../base/redux';
6
+import { ASPECT_RATIO_NARROW } from '../base/responsive-ui/constants';
6 7
 
7 8
 export * from './functions.any';
8 9
 
@@ -59,3 +60,31 @@ export function shouldRemoteVideosBeVisible(state: Object) {
59 60
 
60 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 Целия файл

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

+ 5
- 7
react/features/filmstrip/reducer.js Целия файл

@@ -118,13 +118,9 @@ ReducerRegistry.register(
118 118
             };
119 119
         case SET_REMOTE_PARTICIPANTS: {
120 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 125
             return { ...state };
130 126
         }
@@ -167,7 +163,9 @@ ReducerRegistry.register(
167 163
             }
168 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 Целия файл

@@ -1,3 +1,42 @@
1 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 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 Целия файл

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

+ 11
- 17
react/features/video-menu/components/native/RemoteVideoMenu.js Целия файл

@@ -42,9 +42,9 @@ type Props = {
42 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 50
      * The color-schemed stylesheet of the BottomSheet.
@@ -79,12 +79,7 @@ type Props = {
79 79
     /**
80 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 85
 // eslint-disable-next-line prefer-const
@@ -117,12 +112,12 @@ class RemoteVideoMenu extends PureComponent<Props> {
117 112
             _disableRemoteMute,
118 113
             _disableGrantModerator,
119 114
             _isParticipantAvailable,
120
-            participant
115
+            participantId
121 116
         } = this.props;
122 117
         const buttonProps = {
123 118
             afterClick: this._onCancel,
124 119
             showLabel: true,
125
-            participantID: participant.id,
120
+            participantID: participantId,
126 121
             styles: this.props._bottomSheetStyles.buttons
127 122
         };
128 123
 
@@ -141,7 +136,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
141 136
                 <PrivateMessageButton { ...buttonProps } />
142 137
                 <ConnectionStatusButton { ...buttonProps } />
143 138
                 {/* <Divider style = { styles.divider } />*/}
144
-                {/* <VolumeSlider participantID = { _participantID } />*/}
139
+                {/* <VolumeSlider participantID = { participantId } />*/}
145 140
             </BottomSheet>
146 141
         );
147 142
     }
@@ -172,7 +167,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
172 167
      * @returns {React$Element}
173 168
      */
174 169
     _renderMenuHeader() {
175
-        const { _bottomSheetStyles, participant } = this.props;
170
+        const { _bottomSheetStyles, participantId } = this.props;
176 171
 
177 172
         return (
178 173
             <View
@@ -180,7 +175,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
180 175
                     _bottomSheetStyles.sheet,
181 176
                     styles.participantNameContainer ] }>
182 177
                 <Avatar
183
-                    participantId = { participant.id }
178
+                    participantId = { participantId }
184 179
                     size = { AVATAR_SIZE } />
185 180
                 <Text style = { styles.participantNameLabel }>
186 181
                     { this.props._participantDisplayName }
@@ -200,9 +195,9 @@ class RemoteVideoMenu extends PureComponent<Props> {
200 195
  */
201 196
 function _mapStateToProps(state, ownProps) {
202 197
     const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
203
-    const { participant } = ownProps;
198
+    const { participantId } = ownProps;
204 199
     const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
205
-    const isParticipantAvailable = getParticipantById(state, participant.id);
200
+    const isParticipantAvailable = getParticipantById(state, participantId);
206 201
     let { disableKick } = remoteVideoMenu;
207 202
 
208 203
     disableKick = disableKick || !kickOutEnabled;
@@ -213,8 +208,7 @@ function _mapStateToProps(state, ownProps) {
213 208
         _disableRemoteMute: Boolean(disableRemoteMute),
214 209
         _isOpen: isDialogOpen(state, RemoteVideoMenu_),
215 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 Целия файл

@@ -32,9 +32,9 @@ type Props = {
32 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 40
      * The color-schemed stylesheet of the BottomSheet.
@@ -55,11 +55,6 @@ type Props = {
55 55
      * Display name of the participant retrieved from Redux.
56 56
      */
57 57
     _participantDisplayName: string,
58
-
59
-    /**
60
-     * The ID of the participant.
61
-     */
62
-    _participantID: ?string,
63 58
 }
64 59
 
65 60
 // eslint-disable-next-line prefer-const
@@ -89,13 +84,13 @@ class SharedVideoMenu extends PureComponent<Props> {
89 84
     render() {
90 85
         const {
91 86
             _isParticipantAvailable,
92
-            participant
87
+            participantId
93 88
         } = this.props;
94 89
 
95 90
         const buttonProps = {
96 91
             afterClick: this._onCancel,
97 92
             showLabel: true,
98
-            participantID: participant.id,
93
+            participantID: participantId,
99 94
             styles: this.props._bottomSheetStyles.buttons
100 95
         };
101 96
 
@@ -136,7 +131,7 @@ class SharedVideoMenu extends PureComponent<Props> {
136 131
      * @returns {React$Element}
137 132
      */
138 133
     _renderMenuHeader() {
139
-        const { _bottomSheetStyles, participant } = this.props;
134
+        const { _bottomSheetStyles, participantId } = this.props;
140 135
 
141 136
         return (
142 137
             <View
@@ -144,7 +139,7 @@ class SharedVideoMenu extends PureComponent<Props> {
144 139
                     _bottomSheetStyles.sheet,
145 140
                     styles.participantNameContainer ] }>
146 141
                 <Avatar
147
-                    participantId = { participant.id }
142
+                    participantId = { participantId }
148 143
                     size = { AVATAR_SIZE } />
149 144
                 <Text style = { styles.participantNameLabel }>
150 145
                     { this.props._participantDisplayName }
@@ -163,15 +158,14 @@ class SharedVideoMenu extends PureComponent<Props> {
163 158
  * @returns {Props}
164 159
  */
165 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 164
     return {
170 165
         _bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
171 166
         _isOpen: isDialogOpen(state, SharedVideoMenu_),
172 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 Целия файл

@@ -191,12 +191,7 @@ function _updateReceiverVideoConstraints({ getState }) {
191 191
     const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
192 192
     const { participantId: largeVideoParticipantId } = state['features/large-video'];
193 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 196
     const receiverConstraints = {
202 197
         constraints: {},

Loading…
Отказ
Запис