Browse Source

[RN] Implement tile view

* feat(tile-view): initial implementation for mobile

- Create a tile view component for displaying thumbnails in a
  two-dimensional grid.
- Update the existing TileViewButton so it shows a label in the
  overflow menu.
- Modify conference so it can display TileView while hiding
  Filmstrip.
- Modify Thumbnail so its width/height can be set and to prevent
  pinning while in tile view mode.

* use style array for thumbnail styles

* change ternary to math.min for expressiveness

* use dimensiondetector

* pass explicit disableTint prop

* use makeAspectRatioAware instead of aspectRatio prop

* update docs

* fix docs again (fix laziest copy/paste job I've ever done)

* large-video: rename onPress prop to onClick

* change forEach to for...of

* use truthy check fallthrough logic instead of explicit if

* put tile view button second to last in menu

* move spacer to a constant

* the magical incantation to make flow shut up
master
virtuacoplenny 6 years ago
parent
commit
c25d6eb9a8

+ 59
- 42
react/features/base/react/components/AbstractContainer.js View File

@@ -1,54 +1,71 @@
1 1
 /* @flow */
2 2
 
3
-import PropTypes from 'prop-types';
4 3
 import React, { Component } from 'react';
5 4
 
5
+/**
6
+ * {@code AbstractContainer} component's property types.
7
+ */
8
+export type Props = {
9
+
10
+    /**
11
+     * An optional accessibility label to apply to the container root.
12
+     */
13
+    accessibilityLabel?: string,
14
+
15
+    /**
16
+     * Whether or not this element is an accessibility element.
17
+     */
18
+    accessible?: boolean,
19
+
20
+    /**
21
+     * React Elements to display within the component.
22
+     */
23
+    children: React$Node | Object,
24
+
25
+    /**
26
+     * The event handler/listener to be invoked when this
27
+     * {@code AbstractContainer} is clicked on Web or pressed on React
28
+     * Native. If {@code onClick} is defined and {@link touchFeedback} is
29
+     * undefined, {@code touchFeedback} is considered defined as
30
+     * {@code true}.
31
+     */
32
+    onClick?: ?Function,
33
+
34
+    /**
35
+     * The style (as in stylesheet) to be applied to this
36
+     * {@code AbstractContainer}.
37
+     */
38
+    style?: Array<?string> | Object,
39
+
40
+    /**
41
+     * If this instance is to provide visual feedback when touched, then
42
+     * {@code true}; otherwise, {@code false}. If {@code touchFeedback} is
43
+     * undefined and {@link onClick} is defined, {@code touchFeedback} is
44
+     * considered defined as {@code true}.
45
+     */
46
+    touchFeedback?: ?Function,
47
+
48
+    /**
49
+     * Color to display when clicked.
50
+     */
51
+    underlayColor?: string,
52
+
53
+    /**
54
+     * If this {@code AbstractContainer} is to be visible, then {@code true}
55
+     * or {@code false} if this instance is to be hidden or not rendered at
56
+     * all.
57
+     */
58
+    visible?: ?boolean
59
+};
60
+
61
+
6 62
 /**
7 63
  * Abstract (base) class for container of React {@link Component} children with
8 64
  * a style.
9 65
  *
10 66
  * @extends Component
11 67
  */
12
-export default class AbstractContainer extends Component<*> {
13
-    /**
14
-     * {@code AbstractContainer} component's property types.
15
-     *
16
-     * @static
17
-     */
18
-    static propTypes = {
19
-        children: PropTypes.node,
20
-
21
-        /**
22
-         * The event handler/listener to be invoked when this
23
-         * {@code AbstractContainer} is clicked on Web or pressed on React
24
-         * Native. If {@code onClick} is defined and {@link touchFeedback} is
25
-         * undefined, {@code touchFeedback} is considered defined as
26
-         * {@code true}.
27
-         */
28
-        onClick: PropTypes.func,
29
-
30
-        /**
31
-         * The style (as in stylesheet) to be applied to this
32
-         * {@code AbstractContainer}.
33
-         */
34
-        style: PropTypes.object,
35
-
36
-        /**
37
-         * If this instance is to provide visual feedback when touched, then
38
-         * {@code true}; otherwise, {@code false}. If {@code touchFeedback} is
39
-         * undefined and {@link onClick} is defined, {@code touchFeedback} is
40
-         * considered defined as {@code true}.
41
-         */
42
-        touchFeedback: PropTypes.bool,
43
-
44
-        /**
45
-         * If this {@code AbstractContainer} is to be visible, then {@code true}
46
-         * or {@code false} if this instance is to be hidden or not rendered at
47
-         * all.
48
-         */
49
-        visible: PropTypes.bool
50
-    };
51
-
68
+export default class AbstractContainer<P: Props> extends Component<P> {
52 69
     /**
53 70
      * Renders this {@code AbstractContainer} as a React {@code Component} of a
54 71
      * specific type.
@@ -61,7 +78,7 @@ export default class AbstractContainer extends Component<*> {
61 78
      * @protected
62 79
      * @returns {ReactElement}
63 80
      */
64
-    _render(type, props) {
81
+    _render(type, props?: P) {
65 82
         const {
66 83
             children,
67 84
 

+ 2
- 8
react/features/base/react/components/native/Container.js View File

@@ -8,20 +8,14 @@ import {
8 8
 } from 'react-native';
9 9
 
10 10
 import AbstractContainer from '../AbstractContainer';
11
+import type { Props } from '../AbstractContainer';
11 12
 
12 13
 /**
13 14
  * Represents a container of React Native/mobile {@link Component} children.
14 15
  *
15 16
  * @extends AbstractContainer
16 17
  */
17
-export default class Container extends AbstractContainer {
18
-    /**
19
-     * {@code Container} component's property types.
20
-     *
21
-     * @static
22
-     */
23
-    static propTypes = AbstractContainer.propTypes;
24
-
18
+export default class Container<P: Props> extends AbstractContainer<P> {
25 19
     /**
26 20
      * Implements React's {@link Component#render()}.
27 21
      *

+ 2
- 1
react/features/base/react/components/web/Container.js View File

@@ -1,13 +1,14 @@
1 1
 /* @flow */
2 2
 
3 3
 import AbstractContainer from '../AbstractContainer';
4
+import type { Props } from '../AbstractContainer';
4 5
 
5 6
 /**
6 7
  * Represents a container of React/Web {@link Component} children with a style.
7 8
  *
8 9
  * @extends AbstractContainer
9 10
  */
10
-export default class Container extends AbstractContainer {
11
+export default class Container<P: Props> extends AbstractContainer<P> {
11 12
     /**
12 13
      * {@code Container} component's property types.
13 14
      *

+ 38
- 7
react/features/conference/components/Conference.native.js View File

@@ -17,12 +17,18 @@ import {
17 17
 import { TestConnectionInfo } from '../../base/testing';
18 18
 import { createDesiredLocalTracks } from '../../base/tracks';
19 19
 import { ConferenceNotification } from '../../calendar-sync';
20
-import { FILMSTRIP_SIZE, Filmstrip, isFilmstripVisible } from '../../filmstrip';
20
+import {
21
+    FILMSTRIP_SIZE,
22
+    Filmstrip,
23
+    isFilmstripVisible,
24
+    TileView
25
+} from '../../filmstrip';
21 26
 import { LargeVideo } from '../../large-video';
22 27
 import { CalleeInfoContainer } from '../../invite';
23 28
 import { NotificationsContainer } from '../../notifications';
24 29
 import { Captions } from '../../subtitles';
25 30
 import { setToolboxVisible, Toolbox } from '../../toolbox';
31
+import { shouldDisplayTileView } from '../../video-layout';
26 32
 
27 33
 import styles from './styles';
28 34
 
@@ -115,6 +121,13 @@ type Props = {
115 121
      */
116 122
     _setToolboxVisible: Function,
117 123
 
124
+    /**
125
+     * Whether or not the layout should change to support tile view mode.
126
+     *
127
+     * @private
128
+     */
129
+    _shouldDisplayTileView: boolean,
130
+
118 131
     /**
119 132
      * The indicator which determines whether the Toolbox is visible.
120 133
      *
@@ -252,6 +265,12 @@ class Conference extends Component<Props> {
252 265
      * @returns {ReactElement}
253 266
      */
254 267
     render() {
268
+        const {
269
+            _connecting,
270
+            _reducedUI,
271
+            _shouldDisplayTileView
272
+        } = this.props;
273
+
255 274
         return (
256 275
             <Container style = { styles.conference }>
257 276
                 <StatusBar
@@ -261,20 +280,23 @@ class Conference extends Component<Props> {
261 280
 
262 281
                 {/*
263 282
                   * The LargeVideo is the lowermost stacking layer.
264
-                  */}
265
-                <LargeVideo onPress = { this._onClick } />
283
+                  */
284
+                    _shouldDisplayTileView
285
+                        ? <TileView onClick = { this._onClick } />
286
+                        : <LargeVideo onClick = { this._onClick } />
287
+                }
266 288
 
267 289
                 {/*
268 290
                   * If there is a ringing call, show the callee's info.
269 291
                   */
270
-                    this.props._reducedUI || <CalleeInfoContainer />
292
+                    _reducedUI || <CalleeInfoContainer />
271 293
                 }
272 294
 
273 295
                 {/*
274 296
                   * The activity/loading indicator goes above everything, except
275 297
                   * the toolbox/toolbars and the dialogs.
276 298
                   */
277
-                    this.props._connecting
299
+                    _connecting
278 300
                         && <TintedView>
279 301
                             <LoadingIndicator />
280 302
                         </TintedView>
@@ -304,8 +326,9 @@ class Conference extends Component<Props> {
304 326
                       * name and grouping stem from the fact that these two
305 327
                       * React Components depict the videos of the conference's
306 328
                       * participants.
307
-                      */}
308
-                    <Filmstrip />
329
+                      */
330
+                        _shouldDisplayTileView ? undefined : <Filmstrip />
331
+                    }
309 332
                 </View>
310 333
 
311 334
                 <TestConnectionInfo />
@@ -548,6 +571,14 @@ function _mapStateToProps(state) {
548 571
          */
549 572
         _room: room,
550 573
 
574
+        /**
575
+         * Whether or not the layout should change to support tile view mode.
576
+         *
577
+         * @private
578
+         * @type {boolean}
579
+         */
580
+        _shouldDisplayTileView: shouldDisplayTileView(state),
581
+
551 582
         /**
552 583
          * The indicator which determines whether the Toolbox is visible.
553 584
          *

+ 71
- 32
react/features/filmstrip/components/native/Thumbnail.js View File

@@ -1,4 +1,5 @@
1
-import PropTypes from 'prop-types';
1
+// @flow
2
+
2 3
 import React, { Component } from 'react';
3 4
 import { connect } from 'react-redux';
4 5
 
@@ -19,30 +20,66 @@ import styles from './styles';
19 20
 import VideoMutedIndicator from './VideoMutedIndicator';
20 21
 
21 22
 /**
22
- * React component for video thumbnail.
23
- *
24
- * @extends Component
23
+ * Thumbnail component's property types.
25 24
  */
26
-class Thumbnail extends Component {
25
+type Props = {
26
+
27 27
     /**
28
-     * Thumbnail component's property types.
29
-     *
30
-     * @static
28
+     * The Redux representation of the participant's audio track.
31 29
      */
32
-    static propTypes = {
33
-        _audioTrack: PropTypes.object,
34
-        _largeVideo: PropTypes.object,
35
-        _videoTrack: PropTypes.object,
36
-        dispatch: PropTypes.func,
37
-        participant: PropTypes.object
38
-    };
30
+    _audioTrack: Object,
31
+
32
+    /**
33
+     * The Redux representation of the state "features/large-video".
34
+     */
35
+    _largeVideo: Object,
36
+
37
+    /**
38
+     * The Redux representation of the participant's video track.
39
+     */
40
+    _videoTrack: Object,
41
+
42
+    /**
43
+     * If true, tapping on the thumbnail will not pin the participant to large
44
+     * video. By default tapping does pin the participant.
45
+     */
46
+    disablePin?: boolean,
47
+
48
+    /**
49
+     * If true, there will be no color overlay (tint) on the thumbnail
50
+     * indicating the participant associated with the thumbnail is displayed on
51
+     * large video. By default there will be a tint.
52
+     */
53
+    disableTint?: boolean,
54
+
55
+    /**
56
+     * Invoked to trigger state changes in Redux.
57
+     */
58
+    dispatch: Dispatch<*>,
59
+
60
+    /**
61
+     * The Redux representation of the participant to display.
62
+     */
63
+    participant: Object,
64
+
65
+    /**
66
+     * Optional styling to add or override on the Thumbnail component root.
67
+     */
68
+    styleOverrides?: Object
69
+};
39 70
 
71
+/**
72
+ * React component for video thumbnail.
73
+ *
74
+ * @extends Component
75
+ */
76
+class Thumbnail extends Component<Props> {
40 77
     /**
41 78
      * Initializes new Video Thumbnail component.
42 79
      *
43 80
      * @param {Object} props - Component props.
44 81
      */
45
-    constructor(props) {
82
+    constructor(props: Props) {
46 83
         super(props);
47 84
 
48 85
         // Bind event handlers so they are only bound once for every instance.
@@ -56,19 +93,14 @@ class Thumbnail extends Component {
56 93
      * @returns {ReactElement}
57 94
      */
58 95
     render() {
59
-        const audioTrack = this.props._audioTrack;
60
-        const largeVideo = this.props._largeVideo;
61
-        const participant = this.props.participant;
62
-        const videoTrack = this.props._videoTrack;
63
-
64
-        let style = styles.thumbnail;
65
-
66
-        if (participant.pinned) {
67
-            style = {
68
-                ...style,
69
-                ...styles.thumbnailPinned
70
-            };
71
-        }
96
+        const {
97
+            _audioTrack: audioTrack,
98
+            _largeVideo: largeVideo,
99
+            _videoTrack: videoTrack,
100
+            disablePin,
101
+            disableTint,
102
+            participant
103
+        } = this.props;
72 104
 
73 105
         // We don't render audio in any of the following:
74 106
         // 1. The audio (source) is muted. There's no practical reason (that we
@@ -85,8 +117,13 @@ class Thumbnail extends Component {
85 117
 
86 118
         return (
87 119
             <Container
88
-                onClick = { this._onClick }
89
-                style = { style }>
120
+                onClick = { disablePin ? undefined : this._onClick }
121
+                style = { [
122
+                    styles.thumbnail,
123
+                    participant.pinned && !disablePin
124
+                        ? styles.thumbnailPinned : null,
125
+                    this.props.styleOverrides || null
126
+                ] }>
90 127
 
91 128
                 { renderAudio
92 129
                     && <Audio
@@ -96,7 +133,7 @@ class Thumbnail extends Component {
96 133
                 <ParticipantView
97 134
                     avatarSize = { AVATAR_SIZE }
98 135
                     participantId = { participantId }
99
-                    tintEnabled = { participantInLargeVideo }
136
+                    tintEnabled = { participantInLargeVideo && !disableTint }
100 137
                     zOrder = { 1 } />
101 138
 
102 139
                 { participant.role === PARTICIPANT_ROLE.MODERATOR
@@ -117,6 +154,8 @@ class Thumbnail extends Component {
117 154
         );
118 155
     }
119 156
 
157
+    _onClick: () => void;
158
+
120 159
     /**
121 160
      * Handles click/tap event on the thumbnail.
122 161
      *

+ 337
- 0
react/features/filmstrip/components/native/TileView.js View File

@@ -0,0 +1,337 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import {
5
+    ScrollView,
6
+    TouchableWithoutFeedback,
7
+    View
8
+} from 'react-native';
9
+import { connect } from 'react-redux';
10
+
11
+import {
12
+    getNearestReceiverVideoQualityLevel,
13
+    setMaxReceiverVideoQuality
14
+} from '../../../base/conference';
15
+import {
16
+    DimensionsDetector,
17
+    isNarrowAspectRatio,
18
+    makeAspectRatioAware
19
+} from '../../../base/responsive-ui';
20
+
21
+import Thumbnail from './Thumbnail';
22
+import styles from './styles';
23
+
24
+/**
25
+ * The type of the React {@link Component} props of {@link TileView}.
26
+ */
27
+type Props = {
28
+
29
+    /**
30
+     * The participants in the conference.
31
+     */
32
+    _participants: Array<Object>,
33
+
34
+    /**
35
+     * Invoked to update the receiver video quality.
36
+     */
37
+    dispatch: Dispatch<*>,
38
+
39
+    /**
40
+     * Callback to invoke when tile view is tapped.
41
+     */
42
+    onClick: Function
43
+};
44
+
45
+/**
46
+ * The type of the React {@link Component} state of {@link TileView}.
47
+ */
48
+type State = {
49
+
50
+    /**
51
+     * The available width for {@link TileView} to occupy.
52
+     */
53
+    height: number,
54
+
55
+    /**
56
+     * The available height for {@link TileView} to occupy.
57
+     */
58
+    width: number
59
+};
60
+
61
+/**
62
+ * The margin for each side of the tile view. Taken away from the available
63
+ * height and width for the tile container to display in.
64
+ *
65
+ * @private
66
+ * @type {number}
67
+ */
68
+const MARGIN = 10;
69
+
70
+/**
71
+ * The aspect ratio the tiles should display in.
72
+ *
73
+ * @private
74
+ * @type {number}
75
+ */
76
+const TILE_ASPECT_RATIO = 1;
77
+
78
+/**
79
+ * Implements a React {@link Component} which displays thumbnails in a two
80
+ * dimensional grid.
81
+ *
82
+ * @extends Component
83
+ */
84
+class TileView extends Component<Props, State> {
85
+    state = {
86
+        height: 0,
87
+        width: 0
88
+    };
89
+
90
+    /**
91
+     * Initializes a new {@code TileView} instance.
92
+     *
93
+     * @param {Object} props - The read-only properties with which the new
94
+     * instance is to be initialized.
95
+     */
96
+    constructor(props: Props) {
97
+        super(props);
98
+
99
+        // Bind event handler so it is only bound once per instance.
100
+        this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
101
+    }
102
+
103
+    /**
104
+     * Implements React's {@link Component#componentDidMount}.
105
+     *
106
+     * @inheritdoc
107
+     */
108
+    componentDidMount() {
109
+        this._updateReceiverQuality();
110
+    }
111
+
112
+    /**
113
+     * Implements React's {@link Component#componentDidUpdate}.
114
+     *
115
+     * @inheritdoc
116
+     */
117
+    componentDidUpdate() {
118
+        this._updateReceiverQuality();
119
+    }
120
+
121
+    /**
122
+     * Implements React's {@link Component#render()}.
123
+     *
124
+     * @inheritdoc
125
+     * @returns {ReactElement}
126
+     */
127
+    render() {
128
+        const { onClick } = this.props;
129
+        const { height, width } = this.state;
130
+        const rowElements = this._groupIntoRows(
131
+            this._renderThumbnails(), this._getColumnCount());
132
+
133
+        return (
134
+            <DimensionsDetector
135
+                onDimensionsChanged = { this._onDimensionsChanged }>
136
+                <ScrollView
137
+                    style = {{
138
+                        ...styles.tileView,
139
+                        height,
140
+                        width
141
+                    }}>
142
+                    <TouchableWithoutFeedback onPress = { onClick }>
143
+                        <View
144
+                            style = {{
145
+                                ...styles.tileViewRows,
146
+                                minHeight: height,
147
+                                minWidth: width
148
+                            }}>
149
+                            { rowElements }
150
+                        </View>
151
+                    </TouchableWithoutFeedback>
152
+                </ScrollView>
153
+            </DimensionsDetector>
154
+        );
155
+    }
156
+
157
+    /**
158
+     * Returns how many columns should be displayed for tile view.
159
+     *
160
+     * @returns {number}
161
+     * @private
162
+     */
163
+    _getColumnCount() {
164
+        const participantCount = this.props._participants.length;
165
+
166
+        // For narrow view, tiles should stack on top of each other for a lonely
167
+        // call and a 1:1 call. Otherwise tiles should be grouped into rows of
168
+        // two.
169
+        if (isNarrowAspectRatio(this)) {
170
+            return participantCount >= 3 ? 2 : 1;
171
+        }
172
+
173
+        if (participantCount === 4) {
174
+            // In wide view, a four person call should display as a 2x2 grid.
175
+            return 2;
176
+        }
177
+
178
+        return Math.min(3, participantCount);
179
+    }
180
+
181
+    /**
182
+     * Returns all participants with the local participant at the end.
183
+     *
184
+     * @private
185
+     * @returns {Participant[]}
186
+     */
187
+    _getSortedParticipants() {
188
+        const participants = [];
189
+        let localParticipant;
190
+
191
+        for (const participant of this.props._participants) {
192
+            if (participant.local) {
193
+                localParticipant = participant;
194
+            } else {
195
+                participants.push(participant);
196
+            }
197
+        }
198
+
199
+        localParticipant && participants.push(localParticipant);
200
+
201
+        return participants;
202
+    }
203
+
204
+    /**
205
+     * Calculate the height and width for the tiles.
206
+     *
207
+     * @private
208
+     * @returns {Object}
209
+     */
210
+    _getTileDimensions() {
211
+        const { _participants } = this.props;
212
+        const { height, width } = this.state;
213
+        const columns = this._getColumnCount();
214
+        const participantCount = _participants.length;
215
+        const heightToUse = height - (MARGIN * 2);
216
+        const widthToUse = width - (MARGIN * 2);
217
+        let tileWidth;
218
+
219
+        // If there is going to be at least two rows, ensure that at least two
220
+        // rows display fully on screen.
221
+        if (participantCount / columns > 1) {
222
+            tileWidth
223
+                = Math.min(widthToUse / columns, heightToUse / 2);
224
+        } else {
225
+            tileWidth = Math.min(widthToUse / columns, heightToUse);
226
+        }
227
+
228
+        return {
229
+            height: tileWidth / TILE_ASPECT_RATIO,
230
+            width: tileWidth
231
+        };
232
+    }
233
+
234
+    /**
235
+     * Splits a list of thumbnails into React Elements with a maximum of
236
+     * {@link rowLength} thumbnails in each.
237
+     *
238
+     * @param {Array} thumbnails - The list of thumbnails that should be split
239
+     * into separate row groupings.
240
+     * @param {number} rowLength - How many thumbnails should be in each row.
241
+     * @private
242
+     * @returns {ReactElement[]}
243
+     */
244
+    _groupIntoRows(thumbnails, rowLength) {
245
+        const rowElements = [];
246
+
247
+        for (let i = 0; i < thumbnails.length; i++) {
248
+            if (i % rowLength === 0) {
249
+                const thumbnailsInRow
250
+                    = thumbnails.slice(i, i + rowLength);
251
+
252
+                rowElements.push(
253
+                    <View
254
+                        key = { rowElements.length }
255
+                        style = { styles.tileViewRow }>
256
+                        { thumbnailsInRow }
257
+                    </View>
258
+                );
259
+            }
260
+        }
261
+
262
+        return rowElements;
263
+    }
264
+
265
+    _onDimensionsChanged: (width: number, height: number) => void;
266
+
267
+    /**
268
+     * Updates the known available state for {@link TileView} to occupy.
269
+     *
270
+     * @param {number} width - The component's current width.
271
+     * @param {number} height - The component's current height.
272
+     * @private
273
+     * @returns {void}
274
+     */
275
+    _onDimensionsChanged(width: number, height: number) {
276
+        this.setState({
277
+            height,
278
+            width
279
+        });
280
+    }
281
+
282
+    /**
283
+     * Creates React Elements to display each participant in a thumbnail. Each
284
+     * tile will be.
285
+     *
286
+     * @private
287
+     * @returns {ReactElement[]}
288
+     */
289
+    _renderThumbnails() {
290
+        const styleOverrides = {
291
+            aspectRatio: TILE_ASPECT_RATIO,
292
+            flex: 0,
293
+            height: this._getTileDimensions().height,
294
+            width: null
295
+        };
296
+
297
+        return this._getSortedParticipants()
298
+            .map(participant => (
299
+                <Thumbnail
300
+                    disablePin = { true }
301
+                    disableTint = { true }
302
+                    key = { participant.id }
303
+                    participant = { participant }
304
+                    styleOverrides = { styleOverrides } />));
305
+    }
306
+
307
+    /**
308
+     * Sets the receiver video quality based on the dimensions of the thumbnails
309
+     * that are displayed.
310
+     *
311
+     * @private
312
+     * @returns {void}
313
+     */
314
+    _updateReceiverQuality() {
315
+        const { height } = this._getTileDimensions();
316
+        const qualityLevel = getNearestReceiverVideoQualityLevel(height);
317
+
318
+        this.props.dispatch(setMaxReceiverVideoQuality(qualityLevel));
319
+    }
320
+}
321
+
322
+/**
323
+ * Maps (parts of) the redux state to the associated {@code TileView}'s props.
324
+ *
325
+ * @param {Object} state - The redux state.
326
+ * @private
327
+ * @returns {{
328
+ *     _participants: Participant[]
329
+ * }}
330
+ */
331
+function _mapStateToProps(state) {
332
+    return {
333
+        _participants: state['features/base/participants']
334
+    };
335
+}
336
+
337
+export default connect(_mapStateToProps)(makeAspectRatioAware(TileView));

+ 1
- 0
react/features/filmstrip/components/native/index.js View File

@@ -1,2 +1,3 @@
1 1
 export { default as Filmstrip } from './Filmstrip';
2
+export { default as TileView } from './TileView';
2 3
 export { default as styles } from './styles';

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

@@ -145,5 +145,18 @@ export default {
145 145
             width: 5
146 146
         },
147 147
         shadowRadius: 5
148
+    },
149
+
150
+    tileView: {
151
+        alignSelf: 'center'
152
+    },
153
+
154
+    tileViewRows: {
155
+        justifyContent: 'center'
156
+    },
157
+
158
+    tileViewRow: {
159
+        flexDirection: 'row',
160
+        justifyContent: 'center'
148 161
     }
149 162
 };

+ 4
- 4
react/features/large-video/components/LargeVideo.native.js View File

@@ -17,7 +17,7 @@ type Props = {
17 17
     /**
18 18
      * Callback to invoke when the {@code LargeVideo} is clicked/pressed.
19 19
      */
20
-    onPress: Function,
20
+    onClick: Function,
21 21
 
22 22
     /**
23 23
      * The ID of the participant (to be) depicted by LargeVideo.
@@ -114,8 +114,8 @@ class LargeVideo extends Component<Props, State> {
114 114
             useConnectivityInfoLabel
115 115
         } = this.state;
116 116
         const {
117
-            onPress,
118
-            _participantId
117
+            _participantId,
118
+            onClick
119 119
         } = this.props;
120 120
 
121 121
         return (
@@ -123,7 +123,7 @@ class LargeVideo extends Component<Props, State> {
123 123
                 onDimensionsChanged = { this._onDimensionsChanged }>
124 124
                 <ParticipantView
125 125
                     avatarSize = { avatarSize }
126
-                    onPress = { onPress }
126
+                    onPress = { onClick }
127 127
                     participantId = { _participantId }
128 128
                     style = { styles.largeVideo }
129 129
                     testHintId = 'org.jitsi.meet.LargeVideo'

+ 2
- 0
react/features/toolbox/components/native/OverflowMenu.js View File

@@ -9,6 +9,7 @@ import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
9 9
 import { LiveStreamButton, RecordButton } from '../../../recording';
10 10
 import { RoomLockButton } from '../../../room-lock';
11 11
 import { ClosedCaptionButton } from '../../../subtitles';
12
+import { TileViewButton } from '../../../video-layout';
12 13
 
13 14
 import AudioOnlyButton from './AudioOnlyButton';
14 15
 import { overflowMenuItemStyles } from './styles';
@@ -73,6 +74,7 @@ class OverflowMenu extends Component<Props> {
73 74
                 <ClosedCaptionButton { ...buttonProps } />
74 75
                 <RecordButton { ...buttonProps } />
75 76
                 <LiveStreamButton { ...buttonProps } />
77
+                <TileViewButton { ...buttonProps } />
76 78
                 <PictureInPictureButton { ...buttonProps } />
77 79
             </BottomSheet>
78 80
         );

+ 1
- 0
react/features/video-layout/components/TileViewButton.js View File

@@ -38,6 +38,7 @@ type Props = AbstractButtonProps & {
38 38
 class TileViewButton<P: Props> extends AbstractButton<P, *> {
39 39
     accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
40 40
     iconName = 'icon-tiles-many';
41
+    label = 'toolbar.tileViewToggle';
41 42
     toggledIconName = 'icon-tiles-many toggled';
42 43
     tooltip = 'toolbar.tileViewToggle';
43 44
 

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

@@ -74,7 +74,8 @@ export function shouldDisplayTileView(state: Object = {}) {
74 74
     return Boolean(
75 75
         state['features/video-layout']
76 76
             && state['features/video-layout'].tileViewEnabled
77
-            && !state['features/etherpad'].editing
77
+            && (!state['features/etherpad']
78
+                || !state['features/etherpad'].editing)
78 79
 
79 80
             // Truthy check is needed for interfaceConfig to prevent errors on
80 81
             // mobile which does not have interfaceConfig. On web, tile view

Loading…
Cancel
Save