Browse Source

feat(thumbnails) Add changes to mobile context menu

- long touch on thumbnail opens context menu
- hide context menu icon
- add button for connection info to context menu
master
hmuresan 3 years ago
parent
commit
b995221a2b

+ 4
- 0
css/_connection-info.scss View File

@@ -45,6 +45,10 @@
45 45
         @extend .connection-info__icon;
46 46
     }
47 47
 
48
+    &__mobile {
49
+      margin: 15px;
50
+    }
51
+
48 52
     .connection-actions {
49 53
         margin: 10px auto;
50 54
         text-align: center;

+ 2
- 0
react/features/base/connection/actionTypes.js View File

@@ -53,3 +53,5 @@ export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT';
53 53
  * }
54 54
  */
55 55
 export const SET_LOCATION_URL = 'SET_LOCATION_URL';
56
+
57
+export const SHOW_CONNECTION_INFO = 'SHOW_CONNECTION_INFO';

+ 21
- 1
react/features/base/connection/reducer.js View File

@@ -9,7 +9,8 @@ import {
9 9
     CONNECTION_ESTABLISHED,
10 10
     CONNECTION_FAILED,
11 11
     CONNECTION_WILL_CONNECT,
12
-    SET_LOCATION_URL
12
+    SET_LOCATION_URL,
13
+    SHOW_CONNECTION_INFO
13 14
 } from './actionTypes';
14 15
 import type { ConnectionFailedError } from './actions.native';
15 16
 
@@ -37,6 +38,9 @@ ReducerRegistry.register(
37 38
 
38 39
         case SET_ROOM:
39 40
             return _setRoom(state);
41
+
42
+        case SHOW_CONNECTION_INFO:
43
+            return _setShowConnectionInfo(state, action);
40 44
         }
41 45
 
42 46
         return state;
@@ -195,3 +199,19 @@ function _setRoom(state: Object) {
195 199
         passwordRequired: undefined
196 200
     });
197 201
 }
202
+
203
+/**
204
+ * Reduces a specific redux action {@link SHOW_CONNECTION_INFO} of the feature
205
+ * base/connection.
206
+ *
207
+ * @param {Object} state - The redux state of the feature base/connection.
208
+ * @param {Action} action - The redux action {@code SHOW_CONNECTION_INFO} to reduce.
209
+ * @private
210
+ * @returns {Object} The new state of the feature base/connection after the
211
+ * reduction of the specified action.
212
+ */
213
+function _setShowConnectionInfo(
214
+        state: Object,
215
+        { showConnectionInfo }: { showConnectionInfo: boolean }) {
216
+    return set(state, 'showConnectionInfo', showConnectionInfo);
217
+}

+ 21
- 1
react/features/base/popover/components/Popover.web.js View File

@@ -4,6 +4,7 @@ import InlineDialog from '@atlaskit/inline-dialog';
4 4
 import React, { Component } from 'react';
5 5
 
6 6
 import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
7
+import { isMobileBrowser } from '../../environment/utils';
7 8
 
8 9
 /**
9 10
  * A map of dialog positions, relative to trigger, to css classes used to
@@ -63,6 +64,11 @@ type Props = {
63 64
      */
64 65
     id: string,
65 66
 
67
+    /**
68
+    * Callback to invoke when the popover has closed.
69
+    */
70
+    onPopoverClose: Function,
71
+
66 72
     /**
67 73
      * Callback to invoke when the popover has opened.
68 74
      */
@@ -134,6 +140,16 @@ class Popover extends Component<Props, State> {
134 140
         this._onEscKey = this._onEscKey.bind(this);
135 141
     }
136 142
 
143
+    /**
144
+     * Public method for triggering showing the context menu dialog.
145
+     *
146
+     * @returns {void}
147
+     * @public
148
+     */
149
+    showDialog() {
150
+        this.setState({ showDialog: true });
151
+    }
152
+
137 153
     /**
138 154
      * Sets up an event listener to open a drawer when clicking, rather than entering the
139 155
      * overflow area.
@@ -145,7 +161,7 @@ class Popover extends Component<Props, State> {
145 161
      * @returns {void}
146 162
      */
147 163
     componentDidMount() {
148
-        if (this._drawerContainerRef && this._drawerContainerRef.current) {
164
+        if (this._drawerContainerRef && this._drawerContainerRef.current && !isMobileBrowser()) {
149 165
             this._drawerContainerRef.current.addEventListener('click', this._onShowDialog);
150 166
         }
151 167
     }
@@ -232,6 +248,10 @@ class Popover extends Component<Props, State> {
232 248
      */
233 249
     _onHideDialog() {
234 250
         this.setState({ showDialog: false });
251
+
252
+        if (this.props.onPopoverClose) {
253
+            this.props.onPopoverClose();
254
+        }
235 255
     }
236 256
 
237 257
     _onShowDialog: (Object) => void;

+ 6
- 187
react/features/connection-indicator/components/web/ConnectionIndicator.js View File

@@ -6,20 +6,16 @@ import type { Dispatch } from 'redux';
6 6
 import { translate } from '../../../base/i18n';
7 7
 import { Icon, IconConnectionActive, IconConnectionInactive } from '../../../base/icons';
8 8
 import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
9
-import { MEDIA_TYPE } from '../../../base/media';
10 9
 import { getLocalParticipant, getParticipantById } from '../../../base/participants';
11 10
 import { Popover } from '../../../base/popover';
12 11
 import { connect } from '../../../base/redux';
13
-import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
14
-import { ConnectionStatsTable } from '../../../connection-stats';
15
-import { saveLogs } from '../../actions';
16 12
 import AbstractConnectionIndicator, {
17 13
     INDICATOR_DISPLAY_THRESHOLD,
18 14
     type Props as AbstractProps,
19 15
     type State as AbstractState
20 16
 } from '../AbstractConnectionIndicator';
21 17
 
22
-declare var interfaceConfig: Object;
18
+import ConnectionIndicatorContent from './ConnectionIndicatorContent';
23 19
 
24 20
 /**
25 21
  * An array of display configurations for the connection indicator and its bars.
@@ -84,17 +80,6 @@ type Props = AbstractProps & {
84 80
      */
85 81
     dispatch: Dispatch<any>,
86 82
 
87
-    /**
88
-     * Whether or not should display the "Save Logs" link in the local video
89
-     * stats table.
90
-     */
91
-    enableSaveLogs: boolean,
92
-
93
-    /**
94
-     * Whether or not should display the "Show More" link in the local video
95
-     * stats table.
96
-     */
97
-    disableShowMoreStats: boolean,
98 83
 
99 84
     /**
100 85
      * Whether or not clicking the indicator should display a popover for more
@@ -122,27 +107,6 @@ type Props = AbstractProps & {
122 107
      * Invoked to obtain translated strings.
123 108
      */
124 109
     t: Function,
125
-
126
-    /**
127
-     * The video SSRC of this client.
128
-     */
129
-    videoSsrc: number,
130
-
131
-    /**
132
-     * Invoked to save the conference logs.
133
-     */
134
-    _onSaveLogs: Function
135
-};
136
-
137
-/**
138
- * The type of the React {@code Component} state of {@link ConnectionIndicator}.
139
- */
140
-type State = AbstractState & {
141
-
142
-    /**
143
-     * Whether or not the popover content should display additional statistics.
144
-     */
145
-    showMoreStats: boolean
146 110
 };
147 111
 
148 112
 /**
@@ -151,7 +115,7 @@ type State = AbstractState & {
151 115
  *
152 116
  * @extends {Component}
153 117
  */
154
-class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
118
+class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractState> {
155 119
     /**
156 120
      * Initializes a new {@code ConnectionIndicator} instance.
157 121
      *
@@ -164,12 +128,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
164 128
         this.state = {
165 129
             autoHideTimeout: undefined,
166 130
             showIndicator: false,
167
-            showMoreStats: false,
168 131
             stats: {}
169 132
         };
170
-
171
-        // Bind event handlers so they are only bound once for every instance.
172
-        this._onToggleShowMore = this._onToggleShowMore.bind(this);
173 133
     }
174 134
 
175 135
     /**
@@ -189,7 +149,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
189 149
         return (
190 150
             <Popover
191 151
                 className = { rootClassNames }
192
-                content = { this._renderStatisticsTable() }
152
+                content = { <ConnectionIndicatorContent participantId = { this.props.participantId } /> }
193 153
                 disablePopover = { !this.props.enableStatsDisplay }
194 154
                 position = { this.props.statsPopoverPosition }>
195 155
                 <div className = 'popover-trigger'>
@@ -228,43 +188,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
228 188
         return this._getDisplayConfiguration(percent).colorClass;
229 189
     }
230 190
 
231
-    /**
232
-     * Returns a string that describes the current connection status.
233
-     *
234
-     * @private
235
-     * @returns {string}
236
-     */
237
-    _getConnectionStatusTip() {
238
-        let tipKey;
239
-
240
-        switch (this.props._connectionStatus) {
241
-        case JitsiParticipantConnectionStatus.INTERRUPTED:
242
-            tipKey = 'connectionindicator.quality.lost';
243
-            break;
244
-
245
-        case JitsiParticipantConnectionStatus.INACTIVE:
246
-            tipKey = 'connectionindicator.quality.inactive';
247
-            break;
248
-
249
-        default: {
250
-            const { percent } = this.state.stats;
251
-
252
-            if (typeof percent === 'undefined') {
253
-                // If percentage is undefined then there are no stats available
254
-                // yet, likely because only a local connection has been
255
-                // established so far. Assume a strong connection to start.
256
-                tipKey = 'connectionindicator.quality.good';
257
-            } else {
258
-                const config = this._getDisplayConfiguration(percent);
259
-
260
-                tipKey = config.tip;
261
-            }
262
-        }
263
-        }
264
-
265
-        return this.props.t(tipKey);
266
-    }
267
-
268 191
     /**
269 192
      * Get the icon configuration from QUALITY_TO_WIDTH which has a percentage
270 193
      * that matches or exceeds the passed in percentage. The implementation
@@ -297,19 +220,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
297 220
             ? 'show-connection-indicator' : 'hide-connection-indicator';
298 221
     }
299 222
 
300
-    _onToggleShowMore: () => void;
301
-
302
-    /**
303
-     * Callback to invoke when the show more link in the popover content is
304
-     * clicked. Sets the state which will determine if the popover should show
305
-     * additional statistics about the connection.
306
-     *
307
-     * @returns {void}
308
-     */
309
-    _onToggleShowMore() {
310
-        this.setState({ showMoreStats: !this.state.showMoreStats });
311
-    }
312
-
313 223
     /**
314 224
      * Creates a ReactElement for displaying an icon that represents the current
315 225
      * connection quality.
@@ -367,80 +277,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
367 277
             </span>
368 278
         ];
369 279
     }
370
-
371
-    /**
372
-     * Creates a {@code ConnectionStatisticsTable} instance.
373
-     *
374
-     * @returns {ReactElement}
375
-     */
376
-    _renderStatisticsTable() {
377
-        const {
378
-            bandwidth,
379
-            bitrate,
380
-            bridgeCount,
381
-            codec,
382
-            e2eRtt,
383
-            framerate,
384
-            maxEnabledResolution,
385
-            packetLoss,
386
-            region,
387
-            resolution,
388
-            serverRegion,
389
-            transport
390
-        } = this.state.stats;
391
-
392
-        return (
393
-            <ConnectionStatsTable
394
-                audioSsrc = { this.props.audioSsrc }
395
-                bandwidth = { bandwidth }
396
-                bitrate = { bitrate }
397
-                bridgeCount = { bridgeCount }
398
-                codec = { codec }
399
-                connectionSummary = { this._getConnectionStatusTip() }
400
-                disableShowMoreStats = { this.props.disableShowMoreStats }
401
-                e2eRtt = { e2eRtt }
402
-                enableSaveLogs = { this.props.enableSaveLogs }
403
-                framerate = { framerate }
404
-                isLocalVideo = { this.props.isLocalVideo }
405
-                maxEnabledResolution = { maxEnabledResolution }
406
-                onSaveLogs = { this.props._onSaveLogs }
407
-                onShowMore = { this._onToggleShowMore }
408
-                packetLoss = { packetLoss }
409
-                participantId = { this.props.participantId }
410
-                region = { region }
411
-                resolution = { resolution }
412
-                serverRegion = { serverRegion }
413
-                shouldShowMore = { this.state.showMoreStats }
414
-                transport = { transport }
415
-                videoSsrc = { this.props.videoSsrc } />
416
-        );
417
-    }
418
-}
419
-
420
-
421
-/**
422
- * Maps redux actions to the props of the component.
423
- *
424
- * @param {Function} dispatch - The redux action {@code dispatch} function.
425
- * @returns {{
426
- *     _onSaveLogs: Function,
427
- * }}
428
- * @private
429
- */
430
-export function _mapDispatchToProps(dispatch: Dispatch<any>) {
431
-    return {
432
-        /**
433
-         * Saves the conference logs.
434
-         *
435
-         * @returns {Function}
436
-         */
437
-        _onSaveLogs() {
438
-            dispatch(saveLogs());
439
-        }
440
-    };
441 280
 }
442 281
 
443
-
444 282
 /**
445 283
  * Maps part of the Redux state to the props of this component.
446 284
  *
@@ -450,30 +288,11 @@ export function _mapDispatchToProps(dispatch: Dispatch<any>) {
450 288
  */
451 289
 export function _mapStateToProps(state: Object, ownProps: Props) {
452 290
     const { participantId } = ownProps;
453
-    const conference = state['features/base/conference'].conference;
454 291
     const participant
455
-        = typeof participantId === 'undefined' ? getLocalParticipant(state) : getParticipantById(state, participantId);
456
-    const props = {
457
-        _connectionStatus: participant?.connectionStatus,
458
-        enableSaveLogs: state['features/base/config'].enableSaveLogs,
459
-        disableShowMoreStats: state['features/base/config'].disableShowMoreStats
460
-    };
461
-
462
-    if (conference) {
463
-        const firstVideoTrack = getTrackByMediaTypeAndParticipant(
464
-            state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
465
-        const firstAudioTrack = getTrackByMediaTypeAndParticipant(
466
-            state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantId);
467
-
468
-        return {
469
-            ...props,
470
-            audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
471
-            videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
472
-        };
473
-    }
292
+        = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
474 293
 
475 294
     return {
476
-        ...props
295
+        _connectionStatus: participant?.connectionStatus
477 296
     };
478 297
 }
479
-export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ConnectionIndicator));
298
+export default translate(connect(_mapStateToProps)(ConnectionIndicator));

+ 325
- 0
react/features/connection-indicator/components/web/ConnectionIndicatorContent.js View File

@@ -0,0 +1,325 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import type { Dispatch } from 'redux';
5
+
6
+import { translate } from '../../../base/i18n';
7
+import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
8
+import { MEDIA_TYPE } from '../../../base/media';
9
+import { getLocalParticipant, getParticipantById } from '../../../base/participants';
10
+import { connect } from '../../../base/redux';
11
+import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
12
+import { ConnectionStatsTable } from '../../../connection-stats';
13
+import { saveLogs } from '../../actions';
14
+import AbstractConnectionIndicator, {
15
+    INDICATOR_DISPLAY_THRESHOLD,
16
+    type Props as AbstractProps,
17
+    type State as AbstractState
18
+} from '../AbstractConnectionIndicator';
19
+
20
+/**
21
+ * An array of display configurations for the connection indicator and its bars.
22
+ * The ordering is done specifically for faster iteration to find a matching
23
+ * configuration to the current connection strength percentage.
24
+ *
25
+ * @type {Object[]}
26
+ */
27
+const QUALITY_TO_WIDTH: Array<Object> = [
28
+
29
+    // Full (3 bars)
30
+    {
31
+        colorClass: 'status-high',
32
+        percent: INDICATOR_DISPLAY_THRESHOLD,
33
+        tip: 'connectionindicator.quality.good',
34
+        width: '100%'
35
+    },
36
+
37
+    // 2 bars
38
+    {
39
+        colorClass: 'status-med',
40
+        percent: 10,
41
+        tip: 'connectionindicator.quality.nonoptimal',
42
+        width: '66%'
43
+    },
44
+
45
+    // 1 bar
46
+    {
47
+        colorClass: 'status-low',
48
+        percent: 0,
49
+        tip: 'connectionindicator.quality.poor',
50
+        width: '33%'
51
+    }
52
+
53
+    // Note: we never show 0 bars as long as there is a connection.
54
+];
55
+
56
+/**
57
+ * The type of the React {@code Component} props of {@link ConnectionIndicator}.
58
+ */
59
+type Props = AbstractProps & {
60
+
61
+    /**
62
+     * The current condition of the user's connection, matching one of the
63
+     * enumerated values in the library.
64
+     */
65
+    _connectionStatus: string,
66
+
67
+    /**
68
+     * The audio SSRC of this client.
69
+     */
70
+    audioSsrc: number,
71
+
72
+    /**
73
+     * Css class to apply on container
74
+     */
75
+    className: string,
76
+
77
+    /**
78
+     * The Redux dispatch function.
79
+     */
80
+    dispatch: Dispatch<any>,
81
+
82
+    /**
83
+     * Whether or not should display the "Show More" link in the local video
84
+     * stats table.
85
+     */
86
+    disableShowMoreStats: boolean,
87
+
88
+    /**
89
+     * Whether or not should display the "Save Logs" link in the local video
90
+     * stats table.
91
+     */
92
+    enableSaveLogs: boolean,
93
+
94
+    /**
95
+     * Whether or not the displays stats are for local video.
96
+     */
97
+    isLocalVideo: boolean,
98
+
99
+    /**
100
+     * Invoked to obtain translated strings.
101
+     */
102
+    t: Function,
103
+
104
+    /**
105
+     * The video SSRC of this client.
106
+     */
107
+    videoSsrc: number,
108
+
109
+    /**
110
+     * Invoked to save the conference logs.
111
+     */
112
+    _onSaveLogs: Function
113
+};
114
+
115
+/**
116
+ * The type of the React {@code Component} state of {@link ConnectionIndicator}.
117
+ */
118
+type State = AbstractState & {
119
+
120
+    /**
121
+     * Whether or not the popover content should display additional statistics.
122
+     */
123
+    showMoreStats: boolean
124
+};
125
+
126
+/**
127
+ * Implements a React {@link Component} which displays the current connection
128
+ * quality percentage and has a popover to show more detailed connection stats.
129
+ *
130
+ * @extends {Component}
131
+ */
132
+class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, State> {
133
+    /**
134
+     * Initializes a new {@code ConnectionIndicator} instance.
135
+     *
136
+     * @param {Object} props - The read-only properties with which the new
137
+     * instance is to be initialized.
138
+     */
139
+    constructor(props: Props) {
140
+        super(props);
141
+
142
+        this.state = {
143
+            autoHideTimeout: undefined,
144
+            showIndicator: false,
145
+            showMoreStats: false,
146
+            stats: {}
147
+        };
148
+
149
+        // Bind event handlers so they are only bound once for every instance.
150
+        this._onToggleShowMore = this._onToggleShowMore.bind(this);
151
+    }
152
+
153
+    /**
154
+     * Implements React's {@link Component#render()}.
155
+     *
156
+     * @inheritdoc
157
+     * @returns {ReactElement}
158
+     */
159
+    render() {
160
+        const {
161
+            bandwidth,
162
+            bitrate,
163
+            bridgeCount,
164
+            codec,
165
+            e2eRtt,
166
+            framerate,
167
+            maxEnabledResolution,
168
+            packetLoss,
169
+            region,
170
+            resolution,
171
+            serverRegion,
172
+            transport
173
+        } = this.state.stats;
174
+
175
+        return (
176
+            <ConnectionStatsTable
177
+                audioSsrc = { this.props.audioSsrc }
178
+                bandwidth = { bandwidth }
179
+                bitrate = { bitrate }
180
+                bridgeCount = { bridgeCount }
181
+                codec = { codec }
182
+                connectionSummary = { this._getConnectionStatusTip() }
183
+                disableShowMoreStats = { this.props.disableShowMoreStats }
184
+                e2eRtt = { e2eRtt }
185
+                enableSaveLogs = { this.props.enableSaveLogs }
186
+                framerate = { framerate }
187
+                isLocalVideo = { this.props.isLocalVideo }
188
+                maxEnabledResolution = { maxEnabledResolution }
189
+                onSaveLogs = { this.props._onSaveLogs }
190
+                onShowMore = { this._onToggleShowMore }
191
+                packetLoss = { packetLoss }
192
+                participantId = { this.props.participantId }
193
+                region = { region }
194
+                resolution = { resolution }
195
+                serverRegion = { serverRegion }
196
+                shouldShowMore = { this.state.showMoreStats }
197
+                transport = { transport }
198
+                videoSsrc = { this.props.videoSsrc } />
199
+        );
200
+    }
201
+
202
+    /**
203
+     * Returns a string that describes the current connection status.
204
+     *
205
+     * @private
206
+     * @returns {string}
207
+     */
208
+    _getConnectionStatusTip() {
209
+        let tipKey;
210
+
211
+        switch (this.props._connectionStatus) {
212
+        case JitsiParticipantConnectionStatus.INTERRUPTED:
213
+            tipKey = 'connectionindicator.quality.lost';
214
+            break;
215
+
216
+        case JitsiParticipantConnectionStatus.INACTIVE:
217
+            tipKey = 'connectionindicator.quality.inactive';
218
+            break;
219
+
220
+        default: {
221
+            const { percent } = this.state.stats;
222
+
223
+            if (typeof percent === 'undefined') {
224
+                // If percentage is undefined then there are no stats available
225
+                // yet, likely because only a local connection has been
226
+                // established so far. Assume a strong connection to start.
227
+                tipKey = 'connectionindicator.quality.good';
228
+            } else {
229
+                const config = this._getDisplayConfiguration(percent);
230
+
231
+                tipKey = config.tip;
232
+            }
233
+        }
234
+        }
235
+
236
+        return this.props.t(tipKey);
237
+    }
238
+
239
+    /**
240
+     * Get the icon configuration from QUALITY_TO_WIDTH which has a percentage
241
+     * that matches or exceeds the passed in percentage. The implementation
242
+     * assumes QUALITY_TO_WIDTH is already sorted by highest to lowest
243
+     * percentage.
244
+     *
245
+     * @param {number} percent - The connection percentage, out of 100, to find
246
+     * the closest matching configuration for.
247
+     * @private
248
+     * @returns {Object}
249
+     */
250
+    _getDisplayConfiguration(percent: number): Object {
251
+        return QUALITY_TO_WIDTH.find(x => percent >= x.percent) || {};
252
+    }
253
+
254
+
255
+    _onToggleShowMore: () => void;
256
+
257
+    /**
258
+     * Callback to invoke when the show more link in the popover content is
259
+     * clicked. Sets the state which will determine if the popover should show
260
+     * additional statistics about the connection.
261
+     *
262
+     * @returns {void}
263
+     */
264
+    _onToggleShowMore() {
265
+        this.setState({ showMoreStats: !this.state.showMoreStats });
266
+    }
267
+}
268
+
269
+/**
270
+ * Maps redux actions to the props of the component.
271
+ *
272
+ * @param {Function} dispatch - The redux action {@code dispatch} function.
273
+ * @returns {{
274
+ *     _onSaveLogs: Function,
275
+ * }}
276
+ * @private
277
+ */
278
+export function _mapDispatchToProps(dispatch: Dispatch<any>) {
279
+    return {
280
+        /**
281
+         * Saves the conference logs.
282
+         *
283
+         * @returns {Function}
284
+         */
285
+        _onSaveLogs() {
286
+            dispatch(saveLogs());
287
+        }
288
+    };
289
+}
290
+
291
+
292
+/**
293
+ * Maps part of the Redux state to the props of this component.
294
+ *
295
+ * @param {Object} state - The Redux state.
296
+ * @param {Props} ownProps - The own props of the component.
297
+ * @returns {Props}
298
+ */
299
+export function _mapStateToProps(state: Object, ownProps: Props) {
300
+    const { participantId } = ownProps;
301
+    const conference = state['features/base/conference'].conference;
302
+    const participant
303
+        = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
304
+    const props = {
305
+        _connectionStatus: participant?.connectionStatus,
306
+        enableSaveLogs: state['features/base/config'].enableSaveLogs,
307
+        disableShowMoreStats: state['features/base/config'].disableShowMoreStats
308
+    };
309
+
310
+    if (conference) {
311
+        const firstVideoTrack = getTrackByMediaTypeAndParticipant(
312
+            state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
313
+        const firstAudioTrack = getTrackByMediaTypeAndParticipant(
314
+            state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantId);
315
+
316
+        return {
317
+            ...props,
318
+            audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
319
+            videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
320
+        };
321
+    }
322
+
323
+    return props;
324
+}
325
+export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ConnectionIndicatorContent));

+ 3
- 1
react/features/connection-stats/components/ConnectionStatsTable.js View File

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import { isMobileBrowser } from '../../../features/base/environment/utils';
5 6
 import { translate } from '../../base/i18n';
6 7
 
7 8
 /**
@@ -176,10 +177,11 @@ class ConnectionStatsTable extends Component<Props> {
176 177
      */
177 178
     render() {
178 179
         const { isLocalVideo, enableSaveLogs, disableShowMoreStats } = this.props;
180
+        const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
179 181
 
180 182
         return (
181 183
             <div
182
-                className = 'connection-info'
184
+                className = { className }
183 185
                 onClick = { onClick }>
184 186
                 { this._renderStatistics() }
185 187
                 <div className = 'connection-actions'>

+ 96
- 3
react/features/filmstrip/components/web/Thumbnail.js View File

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import { isMobileBrowser } from '../../../../../react/features/base/environment/utils';
5 6
 import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
6 7
 import { AudioLevelIndicator } from '../../../audio-level-indicator';
7 8
 import { Avatar } from '../../../base/avatar';
@@ -33,7 +34,8 @@ import {
33 34
     DISPLAY_MODE_TO_STRING,
34 35
     DISPLAY_VIDEO,
35 36
     DISPLAY_VIDEO_WITH_NAME,
36
-    VIDEO_TEST_EVENTS
37
+    VIDEO_TEST_EVENTS,
38
+    SHOW_TOOLBAR_CONTEXT_MENU_AFTER
37 39
 } from '../../constants';
38 40
 import { isVideoPlayable, computeDisplayMode } from '../../functions';
39 41
 import logger from '../../logger';
@@ -237,6 +239,16 @@ function onClick(event) {
237 239
  * @extends Component
238 240
  */
239 241
 class Thumbnail extends Component<Props, State> {
242
+    /**
243
+     * The long touch setTimeout handler.
244
+     */
245
+    timeoutHandle: Object;
246
+
247
+    /**
248
+     * Reference to local or remote Video Menu trigger button instance.
249
+     */
250
+    videoMenuTriggerRef: Object;
251
+
240 252
     /**
241 253
      * Initializes a new Thumbnail instance.
242 254
      *
@@ -257,7 +269,10 @@ class Thumbnail extends Component<Props, State> {
257 269
             ...state,
258 270
             displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state))
259 271
         };
272
+        this.timeoutHandle = null;
273
+        this.videoMenuTriggerRef = null;
260 274
 
275
+        this._setInstance = this._setInstance.bind(this);
261 276
         this._updateAudioLevel = this._updateAudioLevel.bind(this);
262 277
         this._onCanPlay = this._onCanPlay.bind(this);
263 278
         this._onClick = this._onClick.bind(this);
@@ -265,6 +280,10 @@ class Thumbnail extends Component<Props, State> {
265 280
         this._onMouseEnter = this._onMouseEnter.bind(this);
266 281
         this._onMouseLeave = this._onMouseLeave.bind(this);
267 282
         this._onTestingEvent = this._onTestingEvent.bind(this);
283
+        this._onTouchStart = this._onTouchStart.bind(this);
284
+        this._onTouchEnd = this._onTouchEnd.bind(this);
285
+        this._onTouchMove = this._onTouchMove.bind(this);
286
+        this._showPopupMenu = this._showPopupMenu.bind(this);
268 287
     }
269 288
 
270 289
     /**
@@ -539,6 +558,54 @@ class Thumbnail extends Component<Props, State> {
539 558
         this.setState({ isHovered: false });
540 559
     }
541 560
 
561
+    _showPopupMenu: () => void;
562
+
563
+    /**
564
+     * Triggers showing the popover context menu.
565
+     *
566
+     * @returns {void}
567
+     */
568
+    _showPopupMenu() {
569
+        if (this.videoMenuTriggerRef) {
570
+            this.videoMenuTriggerRef.showContextMenu();
571
+        }
572
+    }
573
+
574
+    _onTouchStart: () => void;
575
+
576
+    /**
577
+     * Set showing popover context menu after x miliseconds.
578
+     *
579
+     * @returns {void}
580
+     */
581
+    _onTouchStart() {
582
+        this.timeoutHandle = setTimeout(this._showPopupMenu, SHOW_TOOLBAR_CONTEXT_MENU_AFTER);
583
+    }
584
+
585
+    _onTouchEnd: () => void;
586
+
587
+    /**
588
+     * Cancel showing popover context menu after x miliseconds if the no. Of miliseconds is not reached yet,
589
+     * or just clears the timeout.
590
+     *
591
+     * @returns {void}
592
+     */
593
+    _onTouchEnd() {
594
+        clearTimeout(this.timeoutHandle);
595
+    }
596
+
597
+    _onTouchMove: () => void;
598
+
599
+    /**
600
+     * Cancel showing Context menu after x miliseconds if the number of miliseconds is not reached
601
+     * before a touch move(drag), or just clears the timeout.
602
+     *
603
+     * @returns {void}
604
+     */
605
+    _onTouchMove() {
606
+        clearTimeout(this.timeoutHandle);
607
+    }
608
+
542 609
     /**
543 610
      * Renders a fake participant (youtube video) thumbnail.
544 611
      *
@@ -709,6 +776,11 @@ class Thumbnail extends Component<Props, State> {
709 776
                 onClick = { this._onClick }
710 777
                 onMouseEnter = { this._onMouseEnter }
711 778
                 onMouseLeave = { this._onMouseLeave }
779
+                { ...(isMobileBrowser() ? {
780
+                    onTouchEnd: this._onTouchEnd,
781
+                    onTouchMove: this._onTouchMove,
782
+                    onTouchStart: this._onTouchStart
783
+                } : {}) }
712 784
                 style = { styles.thumbnail }>
713 785
                 <div className = 'videocontainer__background' />
714 786
                 <span id = 'localVideoWrapper'>
@@ -738,8 +810,10 @@ class Thumbnail extends Component<Props, State> {
738 810
                     <AudioLevelIndicator audioLevel = { audioLevel } />
739 811
                 </span>
740 812
                 <span className = 'localvideomenu'>
741
-                    <LocalVideoMenuTriggerButton />
813
+                    <LocalVideoMenuTriggerButton
814
+                        getRef = { this._setInstance } />
742 815
                 </span>
816
+
743 817
             </span>
744 818
         );
745 819
     }
@@ -783,6 +857,19 @@ class Thumbnail extends Component<Props, State> {
783 857
         dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
784 858
     }
785 859
 
860
+    _setInstance: Object => void;
861
+
862
+    /**
863
+     * Stores the local or remote video menu button instance in a variable.
864
+     *
865
+     * @param {Object} instance - The local or remote video menu trigger instance.
866
+     *
867
+     * @returns {void}
868
+     */
869
+    _setInstance(instance) {
870
+        this.videoMenuTriggerRef = instance;
871
+    }
872
+
786 873
     /**
787 874
      * Renders a remote participant's 'thumbnail.
788 875
      *
@@ -826,6 +913,11 @@ class Thumbnail extends Component<Props, State> {
826 913
                 onClick = { this._onClick }
827 914
                 onMouseEnter = { this._onMouseEnter }
828 915
                 onMouseLeave = { this._onMouseLeave }
916
+                { ...(isMobileBrowser() ? {
917
+                    onTouchEnd: this._onTouchEnd,
918
+                    onTouchMove: this._onTouchMove,
919
+                    onTouchStart: this._onTouchStart
920
+                } : {}) }
829 921
                 style = { styles.thumbnail }>
830 922
                 {
831 923
                     _videoTrack && <VideoTrack
@@ -859,6 +951,7 @@ class Thumbnail extends Component<Props, State> {
859 951
                 </span>
860 952
                 <span className = 'remotevideomenu'>
861 953
                     <RemoteVideoMenuTriggerButton
954
+                        getRef = { this._setInstance }
862 955
                         initialVolumeValue = { _volume }
863 956
                         onVolumeChange = { onVolumeChange }
864 957
                         participantID = { id } />
@@ -982,7 +1075,7 @@ function _mapStateToProps(state, ownProps): Object {
982 1075
     return {
983 1076
         _audioTrack,
984 1077
         _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
985
-        _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
1078
+        _connectionIndicatorDisabled: isMobileBrowser() || interfaceConfig.CONNECTION_INDICATOR_DISABLED,
986 1079
         _currentLayout,
987 1080
         _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
988 1081
         _disableLocalVideoFlip: Boolean(disableLocalVideoFlip),

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

@@ -208,3 +208,10 @@ export const VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN = 10;
208 208
  * @type {number}
209 209
  */
210 210
 export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
211
+
212
+/**
213
+ * Sets after how many ms to show the thumbnail context menu on long touch on mobile.
214
+ *
215
+ * @type {number}
216
+ */
217
+export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;

+ 7
- 5
react/features/toolbox/components/web/Drawer.js View File

@@ -48,22 +48,24 @@ function Drawer({
48 48
     const drawerRef: Object = useRef(null);
49 49
 
50 50
     /**
51
-     * Closes the drawer when clicking outside of it.
51
+     * Closes the drawer when clicking or touching outside of it.
52 52
      *
53
-     * @param {Event} event - Mouse down event object.
53
+     * @param {Event} event - Mouse down/start touch event object.
54 54
      * @returns {void}
55 55
      */
56
-    function handleOutsideClick(event: MouseEvent) {
56
+    function handleOutsideClickOrTouch(event: Event) {
57 57
         if (drawerRef.current && !drawerRef.current.contains(event.target)) {
58 58
             onClose();
59 59
         }
60 60
     }
61 61
 
62 62
     useEffect(() => {
63
-        window.addEventListener('mousedown', handleOutsideClick);
63
+        window.addEventListener('mousedown', handleOutsideClickOrTouch);
64
+        window.addEventListener('touchstart', handleOutsideClickOrTouch);
64 65
 
65 66
         return () => {
66
-            window.removeEventListener('mousedown', handleOutsideClick);
67
+            window.removeEventListener('mousedown', handleOutsideClickOrTouch);
68
+            window.removeEventListener('touchstart', handleOutsideClickOrTouch);
67 69
         };
68 70
     }, [ drawerRef ]);
69 71
 

+ 16
- 0
react/features/video-menu/actions.web.js View File

@@ -1,2 +1,18 @@
1 1
 // @flow
2
+import { SHOW_CONNECTION_INFO } from '../base/connection/actionTypes';
3
+
2 4
 export * from './actions.any';
5
+
6
+/**
7
+ * Sets whether to render the connnection status info into the Popover of the thumbnail or the context menu buttons.
8
+ *
9
+ * @param {boolean} showConnectionInfo - Whether it should show the connection
10
+ * info or the context menu buttons on thumbnail popover.
11
+ * @returns {Object}
12
+ */
13
+export function renderConnectionStatus(showConnectionInfo: boolean) {
14
+    return {
15
+        type: SHOW_CONNECTION_INFO,
16
+        showConnectionInfo
17
+    };
18
+}

+ 48
- 0
react/features/video-menu/components/web/ConnectionStatusButton.js View File

@@ -0,0 +1,48 @@
1
+// @flow
2
+import React, { useCallback } from 'react';
3
+
4
+import { translate } from '../../../base/i18n';
5
+import { IconInfo } from '../../../base/icons';
6
+import { connect } from '../../../base/redux';
7
+import { renderConnectionStatus } from '../../actions.web';
8
+
9
+import VideoMenuButton from './VideoMenuButton';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * The Redux dispatch function.
15
+     */
16
+    dispatch: Function,
17
+
18
+    /**
19
+     * The ID of the participant for which to show connection stats.
20
+     */
21
+    participantId: string,
22
+
23
+    /**
24
+     * The function to be used to translate i18n labels.
25
+     */
26
+    t: Function
27
+};
28
+
29
+
30
+const ConnectionStatusButton = ({
31
+    dispatch,
32
+    participantId,
33
+    t
34
+}: Props) => {
35
+    const onClick = useCallback(() => {
36
+        dispatch(renderConnectionStatus(true));
37
+    }, [ dispatch ]);
38
+
39
+    return (
40
+        <VideoMenuButton
41
+            buttonText = { t('videothumbnail.connectionInfo') }
42
+            icon = { IconInfo }
43
+            id = { `connstatus_${participantId}` }
44
+            onClick = { onClick } />
45
+    );
46
+};
47
+
48
+export default translate(connect()(ConnectionStatusButton));

+ 152
- 28
react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js View File

@@ -1,23 +1,46 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { Component } from 'react';
4 4
 
5
+import { isMobileBrowser } from '../../../base/environment/utils';
5 6
 import { translate } from '../../../base/i18n';
6 7
 import { Icon, IconMenuThumb } from '../../../base/icons';
8
+import {
9
+    getLocalParticipant
10
+} from '../../../base/participants';
7 11
 import { Popover } from '../../../base/popover';
8 12
 import { connect } from '../../../base/redux';
9 13
 import { getLocalVideoTrack } from '../../../base/tracks';
14
+import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
10 15
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
16
+import { renderConnectionStatus } from '../../actions.web';
11 17
 
18
+import ConnectionStatusButton from './ConnectionStatusButton';
12 19
 import FlipLocalVideoButton from './FlipLocalVideoButton';
13 20
 import VideoMenu from './VideoMenu';
14 21
 
22
+
15 23
 /**
16 24
  * The type of the React {@code Component} props of
17 25
  * {@link LocalVideoMenuTriggerButton}.
18 26
  */
19 27
 type Props = {
20 28
 
29
+    /**
30
+     * The redux dispatch function.
31
+     */
32
+     dispatch: Function,
33
+
34
+    /**
35
+     * Gets a ref to the current component instance.
36
+     */
37
+     getRef: Function,
38
+
39
+    /**
40
+     * The id of the local participant.
41
+     */
42
+    _localParticipantId: string,
43
+
21 44
     /**
22 45
      * The position relative to the trigger the local video menu should display
23 46
      * from. Valid values are those supported by AtlasKit
@@ -30,6 +53,11 @@ type Props = {
30 53
      */
31 54
     _overflowDrawer: boolean,
32 55
 
56
+    /**
57
+     * Whether to render the connection info pane.
58
+     */
59
+    _showConnectionInfo: boolean,
60
+
33 61
     /**
34 62
      * Shows/hides the local video flip button.
35 63
      */
@@ -45,33 +73,124 @@ type Props = {
45 73
  * React Component for displaying an icon associated with opening the
46 74
  * the video menu for the local participant.
47 75
  *
48
- * @param {Props} props - The props passed to the component.
49
- * @returns {ReactElement}
76
+ * @extends {Component}
50 77
  */
51
-function LocalVideoMenuTriggerButton(props: Props) {
52
-    return (
53
-        props._showLocalVideoFlipButton
54
-            ? <Popover
55
-                content = {
56
-                    <VideoMenu id = 'localVideoMenu'>
57
-                        <FlipLocalVideoButton />
58
-                    </VideoMenu>
59
-                }
60
-                overflowDrawer = { props._overflowDrawer }
61
-                position = { props._menuPosition }>
62
-                <span
63
-                    className = 'popover-trigger local-video-menu-trigger'>
64
-                    <Icon
65
-                        ariaLabel = { props.t('dialog.localUserControls') }
66
-                        role = 'button'
67
-                        size = '1em'
68
-                        src = { IconMenuThumb }
69
-                        tabIndex = { 0 }
70
-                        title = { props.t('dialog.localUserControls') } />
71
-                </span>
72
-            </Popover>
73
-            : null
74
-    );
78
+class LocalVideoMenuTriggerButton extends Component<Props> {
79
+    /**
80
+     * Reference to the Popover instance.
81
+     */
82
+    popoverRef: Object;
83
+
84
+    /**
85
+     * Initializes a new LocalVideoMenuTriggerButton instance.
86
+     *
87
+     * @param {Object} props - The read-only React Component props with which
88
+     * the new instance is to be initialized.
89
+     */
90
+    constructor(props: Props) {
91
+        super(props);
92
+
93
+        this.popoverRef = React.createRef();
94
+        this._onPopoverClose = this._onPopoverClose.bind(this);
95
+    }
96
+
97
+    /**
98
+     * Triggers showing the popover's context menu.
99
+     *
100
+     * @returns {void}
101
+     */
102
+    showContextMenu() {
103
+        if (this.popoverRef && this.popoverRef.current) {
104
+            this.popoverRef.current.showDialog();
105
+        }
106
+    }
107
+
108
+    /**
109
+     * Calls the ref(instance) getter.
110
+     *
111
+     * @inheritdoc
112
+     * @returns {void}
113
+     */
114
+    componentDidMount() {
115
+        if (this.props.getRef) {
116
+            this.props.getRef(this);
117
+        }
118
+    }
119
+
120
+    /**
121
+     * Calls the ref(instance) getter.
122
+     *
123
+     * @inheritdoc
124
+     * @returns {void}
125
+     */
126
+    componentWillUnmount() {
127
+        if (this.props.getRef) {
128
+            this.props.getRef(null);
129
+        }
130
+    }
131
+
132
+    /**
133
+     * Implements React's {@link Component#render()}.
134
+     *
135
+     * @inheritdoc
136
+     * @returns {ReactElement}
137
+     */
138
+    render() {
139
+        const {
140
+            _localParticipantId,
141
+            _menuPosition,
142
+            _showConnectionInfo,
143
+            _overflowDrawer,
144
+            _showLocalVideoFlipButton,
145
+            t
146
+        } = this.props;
147
+
148
+        const content = _showConnectionInfo
149
+            ? <ConnectionIndicatorContent participantId = { _localParticipantId } />
150
+            : (
151
+                <VideoMenu id = 'localVideoMenu'>
152
+                    <FlipLocalVideoButton />
153
+                    { isMobileBrowser()
154
+                            && <ConnectionStatusButton participantId = { _localParticipantId } />
155
+                    }
156
+                </VideoMenu>
157
+            );
158
+
159
+        return (
160
+            isMobileBrowser() || _showLocalVideoFlipButton
161
+                ? <Popover
162
+                    content = { content }
163
+                    onPopoverClose = { this._onPopoverClose }
164
+                    overflowDrawer = { _overflowDrawer }
165
+                    position = { _menuPosition }
166
+                    ref = { this.popoverRef }>
167
+                    {!isMobileBrowser() && (
168
+                        <span
169
+                            className = 'popover-trigger local-video-menu-trigger'>
170
+                            <Icon
171
+                                ariaLabel = { t('dialog.localUserControls') }
172
+                                role = 'button'
173
+                                size = '1em'
174
+                                src = { IconMenuThumb }
175
+                                tabIndex = { 0 }
176
+                                title = { t('dialog.localUserControls') } />
177
+                        </span>
178
+                    )}
179
+                </Popover>
180
+                : null
181
+        );
182
+    }
183
+
184
+    _onPopoverClose: () => void;
185
+
186
+    /**
187
+     * Render normal context menu next time popover dialog opens.
188
+     *
189
+     * @returns {void}
190
+     */
191
+    _onPopoverClose() {
192
+        this.props.dispatch(renderConnectionStatus(false));
193
+    }
75 194
 }
76 195
 
77 196
 /**
@@ -83,9 +202,12 @@ function LocalVideoMenuTriggerButton(props: Props) {
83 202
  */
84 203
 function _mapStateToProps(state) {
85 204
     const currentLayout = getCurrentLayout(state);
205
+    const localParticipant = getLocalParticipant(state);
86 206
     const { disableLocalVideoFlip } = state['features/base/config'];
87 207
     const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
88 208
     const { overflowDrawer } = state['features/toolbox'];
209
+    const { showConnectionInfo } = state['features/base/connection'];
210
+
89 211
     let _menuPosition;
90 212
 
91 213
     switch (currentLayout) {
@@ -102,7 +224,9 @@ function _mapStateToProps(state) {
102 224
     return {
103 225
         _menuPosition,
104 226
         _showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop',
105
-        _overflowDrawer: overflowDrawer
227
+        _overflowDrawer: overflowDrawer,
228
+        _localParticipantId: localParticipant.id,
229
+        _showConnectionInfo: showConnectionInfo
106 230
     };
107 231
 }
108 232
 

+ 108
- 15
react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js View File

@@ -2,6 +2,9 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import ConnectionIndicatorContent from
6
+    '../../../../features/connection-indicator/components/web/ConnectionIndicatorContent';
7
+import { isMobileBrowser } from '../../../base/environment/utils';
5 8
 import { translate } from '../../../base/i18n';
6 9
 import { Icon, IconMenuThumb } from '../../../base/icons';
7 10
 import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
@@ -9,11 +12,14 @@ import { Popover } from '../../../base/popover';
9 12
 import { connect } from '../../../base/redux';
10 13
 import { requestRemoteControl, stopController } from '../../../remote-control';
11 14
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
15
+import { renderConnectionStatus } from '../../actions.web';
12 16
 
17
+import ConnectionStatusButton from './ConnectionStatusButton';
13 18
 import MuteEveryoneElseButton from './MuteEveryoneElseButton';
14 19
 import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
15 20
 import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
16 21
 
22
+
17 23
 import {
18 24
     GrantModeratorButton,
19 25
     MuteButton,
@@ -26,7 +32,6 @@ import {
26 32
 } from './';
27 33
 
28 34
 declare var $: Object;
29
-declare var interfaceConfig: Object;
30 35
 
31 36
 /**
32 37
  * The type of the React {@code Component} props of
@@ -71,12 +76,16 @@ type Props = {
71 76
      */
72 77
     _remoteControlState: number,
73 78
 
74
-
75 79
     /**
76 80
      * The redux dispatch function.
77 81
      */
78 82
     dispatch: Function,
79 83
 
84
+    /**
85
+     * Gets a ref to the current component instance.
86
+     */
87
+    getRef: Function,
88
+
80 89
     /**
81 90
      * A value between 0 and 1 indicating the volume of the participant's
82 91
      * audio element.
@@ -99,6 +108,11 @@ type Props = {
99 108
      */
100 109
     _participantDisplayName: string,
101 110
 
111
+    /**
112
+     * Whether the popover should render the Connection Info stats.
113
+     */
114
+    _showConnectionInfo: Boolean,
115
+
102 116
     /**
103 117
      * Invoked to obtain translated strings.
104 118
      */
@@ -112,6 +126,59 @@ type Props = {
112 126
  * @extends {Component}
113 127
  */
114 128
 class RemoteVideoMenuTriggerButton extends Component<Props> {
129
+    /**
130
+     * Reference to the Popover instance.
131
+     */
132
+    popoverRef: Object;
133
+
134
+    /**
135
+     * Initializes a new RemoteVideoMenuTriggerButton instance.
136
+     *
137
+     * @param {Object} props - The read-only React Component props with which
138
+     * the new instance is to be initialized.
139
+     */
140
+    constructor(props: Props) {
141
+        super(props);
142
+
143
+        this.popoverRef = React.createRef();
144
+        this._onPopoverClose = this._onPopoverClose.bind(this);
145
+    }
146
+
147
+    /**
148
+     * Triggers showing the popover's context menu.
149
+     *
150
+     * @returns {void}
151
+     */
152
+    showContextMenu() {
153
+        if (this.popoverRef && this.popoverRef.current) {
154
+            this.popoverRef.current.showDialog();
155
+        }
156
+    }
157
+
158
+    /**
159
+     * Calls the ref(instance) getter.
160
+     *
161
+     * @inheritdoc
162
+     * @returns {void}
163
+     */
164
+    componentDidMount() {
165
+        if (this.props.getRef) {
166
+            this.props.getRef(this);
167
+        }
168
+    }
169
+
170
+    /**
171
+     * Calls the ref(instance) getter.
172
+     *
173
+     * @inheritdoc
174
+     * @returns {void}
175
+     */
176
+    componentWillUnmount() {
177
+        if (this.props.getRef) {
178
+            this.props.getRef(null);
179
+        }
180
+    }
181
+
115 182
     /**
116 183
      * Implements React's {@link Component#render()}.
117 184
      *
@@ -119,32 +186,50 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
119 186
      * @returns {ReactElement}
120 187
      */
121 188
     render() {
122
-        const content = this._renderRemoteVideoMenu();
189
+        const { _showConnectionInfo, _participantDisplayName, participantID } = this.props;
190
+        const content = _showConnectionInfo
191
+            ? <ConnectionIndicatorContent participantId = { participantID } />
192
+            : this._renderRemoteVideoMenu();
123 193
 
124 194
         if (!content) {
125 195
             return null;
126 196
         }
127 197
 
128
-        const username = this.props._participantDisplayName;
198
+        const username = _participantDisplayName;
129 199
 
130 200
         return (
131 201
             <Popover
132 202
                 content = { content }
203
+                onPopoverClose = { this._onPopoverClose }
133 204
                 overflowDrawer = { this.props._overflowDrawer }
134
-                position = { this.props._menuPosition }>
135
-                <span className = 'popover-trigger remote-video-menu-trigger'>
136
-                    <Icon
137
-                        ariaLabel = { this.props.t('dialog.remoteUserControls', { username }) }
138
-                        role = 'button'
139
-                        size = '1.4em'
140
-                        src = { IconMenuThumb }
141
-                        tabIndex = { 0 }
142
-                        title = { this.props.t('dialog.remoteUserControls', { username }) } />
143
-                </span>
205
+                position = { this.props._menuPosition }
206
+                ref = { this.popoverRef }>
207
+                {!isMobileBrowser() && (
208
+                    <span className = 'popover-trigger remote-video-menu-trigger'>
209
+                        <Icon
210
+                            ariaLabel = { this.props.t('dialog.remoteUserControls', { username }) }
211
+                            role = 'button'
212
+                            size = '1.4em'
213
+                            src = { IconMenuThumb }
214
+                            tabIndex = { 0 }
215
+                            title = { this.props.t('dialog.remoteUserControls', { username }) } />
216
+                    </span>
217
+                )}
144 218
             </Popover>
145 219
         );
146 220
     }
147 221
 
222
+    _onPopoverClose: () => void;
223
+
224
+    /**
225
+     * Render normal context menu next time popover dialog opens.
226
+     *
227
+     * @returns {void}
228
+     */
229
+    _onPopoverClose() {
230
+        this.props.dispatch(renderConnectionStatus(false));
231
+    }
232
+
148 233
     /**
149 234
      * Creates a new {@code VideoMenu} with buttons for interacting with
150 235
      * the remote participant.
@@ -232,6 +317,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
232 317
                 participantID = { participantID } />
233 318
         );
234 319
 
320
+        if (isMobileBrowser()) {
321
+            buttons.push(
322
+                <ConnectionStatusButton
323
+                    participantId = { participantID } />
324
+            );
325
+        }
235 326
 
236 327
         if (onVolumeChange && typeof initialVolumeValue === 'number' && !isNaN(initialVolumeValue)) {
237 328
             buttons.push(
@@ -276,6 +367,7 @@ function _mapStateToProps(state, ownProps) {
276 367
     const { requestedParticipant, controlled } = controller;
277 368
     const activeParticipant = requestedParticipant || controlled;
278 369
     const { overflowDrawer } = state['features/toolbox'];
370
+    const { showConnectionInfo } = state['features/base/connection'];
279 371
 
280 372
     if (_supportsRemoteControl
281 373
             && ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
@@ -310,7 +402,8 @@ function _mapStateToProps(state, ownProps) {
310 402
         _menuPosition,
311 403
         _overflowDrawer: overflowDrawer,
312 404
         _participantDisplayName,
313
-        _disableGrantModerator: Boolean(disableGrantModerator)
405
+        _disableGrantModerator: Boolean(disableGrantModerator),
406
+        _showConnectionInfo: showConnectionInfo
314 407
     };
315 408
 }
316 409
 

+ 1
- 0
react/features/video-menu/components/web/index.js View File

@@ -1,5 +1,6 @@
1 1
 // @flow
2 2
 
3
+export { default as ConnectionStatusButton } from './ConnectionStatusButton';
3 4
 export { default as GrantModeratorButton } from './GrantModeratorButton';
4 5
 export { default as GrantModeratorDialog } from './GrantModeratorDialog';
5 6
 export { default as KickButton } from './KickButton';

Loading…
Cancel
Save