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
This commit is contained in:
parent
0507f8c2f9
commit
b995221a2b
|
@ -45,6 +45,10 @@
|
|||
@extend .connection-info__icon;
|
||||
}
|
||||
|
||||
&__mobile {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
margin: 10px auto;
|
||||
text-align: center;
|
||||
|
|
|
@ -53,3 +53,5 @@ export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT';
|
|||
* }
|
||||
*/
|
||||
export const SET_LOCATION_URL = 'SET_LOCATION_URL';
|
||||
|
||||
export const SHOW_CONNECTION_INFO = 'SHOW_CONNECTION_INFO';
|
||||
|
|
|
@ -9,7 +9,8 @@ import {
|
|||
CONNECTION_ESTABLISHED,
|
||||
CONNECTION_FAILED,
|
||||
CONNECTION_WILL_CONNECT,
|
||||
SET_LOCATION_URL
|
||||
SET_LOCATION_URL,
|
||||
SHOW_CONNECTION_INFO
|
||||
} from './actionTypes';
|
||||
import type { ConnectionFailedError } from './actions.native';
|
||||
|
||||
|
@ -37,6 +38,9 @@ ReducerRegistry.register(
|
|||
|
||||
case SET_ROOM:
|
||||
return _setRoom(state);
|
||||
|
||||
case SHOW_CONNECTION_INFO:
|
||||
return _setShowConnectionInfo(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
|
@ -195,3 +199,19 @@ function _setRoom(state: Object) {
|
|||
passwordRequired: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific redux action {@link SHOW_CONNECTION_INFO} of the feature
|
||||
* base/connection.
|
||||
*
|
||||
* @param {Object} state - The redux state of the feature base/connection.
|
||||
* @param {Action} action - The redux action {@code SHOW_CONNECTION_INFO} to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/connection after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _setShowConnectionInfo(
|
||||
state: Object,
|
||||
{ showConnectionInfo }: { showConnectionInfo: boolean }) {
|
||||
return set(state, 'showConnectionInfo', showConnectionInfo);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import InlineDialog from '@atlaskit/inline-dialog';
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
|
||||
import { isMobileBrowser } from '../../environment/utils';
|
||||
|
||||
/**
|
||||
* A map of dialog positions, relative to trigger, to css classes used to
|
||||
|
@ -63,6 +64,11 @@ type Props = {
|
|||
*/
|
||||
id: string,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the popover has closed.
|
||||
*/
|
||||
onPopoverClose: Function,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the popover has opened.
|
||||
*/
|
||||
|
@ -134,6 +140,16 @@ class Popover extends Component<Props, State> {
|
|||
this._onEscKey = this._onEscKey.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method for triggering showing the context menu dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
* @public
|
||||
*/
|
||||
showDialog() {
|
||||
this.setState({ showDialog: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an event listener to open a drawer when clicking, rather than entering the
|
||||
* overflow area.
|
||||
|
@ -145,7 +161,7 @@ class Popover extends Component<Props, State> {
|
|||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this._drawerContainerRef && this._drawerContainerRef.current) {
|
||||
if (this._drawerContainerRef && this._drawerContainerRef.current && !isMobileBrowser()) {
|
||||
this._drawerContainerRef.current.addEventListener('click', this._onShowDialog);
|
||||
}
|
||||
}
|
||||
|
@ -232,6 +248,10 @@ class Popover extends Component<Props, State> {
|
|||
*/
|
||||
_onHideDialog() {
|
||||
this.setState({ showDialog: false });
|
||||
|
||||
if (this.props.onPopoverClose) {
|
||||
this.props.onPopoverClose();
|
||||
}
|
||||
}
|
||||
|
||||
_onShowDialog: (Object) => void;
|
||||
|
|
|
@ -6,20 +6,16 @@ import type { Dispatch } from 'redux';
|
|||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconConnectionActive, IconConnectionInactive } from '../../../base/icons';
|
||||
import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
|
||||
import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
||||
import { ConnectionStatsTable } from '../../../connection-stats';
|
||||
import { saveLogs } from '../../actions';
|
||||
import AbstractConnectionIndicator, {
|
||||
INDICATOR_DISPLAY_THRESHOLD,
|
||||
type Props as AbstractProps,
|
||||
type State as AbstractState
|
||||
} from '../AbstractConnectionIndicator';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
import ConnectionIndicatorContent from './ConnectionIndicatorContent';
|
||||
|
||||
/**
|
||||
* An array of display configurations for the connection indicator and its bars.
|
||||
|
@ -84,17 +80,6 @@ type Props = AbstractProps & {
|
|||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Whether or not should display the "Save Logs" link in the local video
|
||||
* stats table.
|
||||
*/
|
||||
enableSaveLogs: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not should display the "Show More" link in the local video
|
||||
* stats table.
|
||||
*/
|
||||
disableShowMoreStats: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not clicking the indicator should display a popover for more
|
||||
|
@ -122,27 +107,6 @@ type Props = AbstractProps & {
|
|||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The video SSRC of this client.
|
||||
*/
|
||||
videoSsrc: number,
|
||||
|
||||
/**
|
||||
* Invoked to save the conference logs.
|
||||
*/
|
||||
_onSaveLogs: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link ConnectionIndicator}.
|
||||
*/
|
||||
type State = AbstractState & {
|
||||
|
||||
/**
|
||||
* Whether or not the popover content should display additional statistics.
|
||||
*/
|
||||
showMoreStats: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -151,7 +115,7 @@ type State = AbstractState & {
|
|||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractState> {
|
||||
/**
|
||||
* Initializes a new {@code ConnectionIndicator} instance.
|
||||
*
|
||||
|
@ -164,12 +128,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
|||
this.state = {
|
||||
autoHideTimeout: undefined,
|
||||
showIndicator: false,
|
||||
showMoreStats: false,
|
||||
stats: {}
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onToggleShowMore = this._onToggleShowMore.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,7 +149,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
|||
return (
|
||||
<Popover
|
||||
className = { rootClassNames }
|
||||
content = { this._renderStatisticsTable() }
|
||||
content = { <ConnectionIndicatorContent participantId = { this.props.participantId } /> }
|
||||
disablePopover = { !this.props.enableStatsDisplay }
|
||||
position = { this.props.statsPopoverPosition }>
|
||||
<div className = 'popover-trigger'>
|
||||
|
@ -228,43 +188,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
|||
return this._getDisplayConfiguration(percent).colorClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string that describes the current connection status.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getConnectionStatusTip() {
|
||||
let tipKey;
|
||||
|
||||
switch (this.props._connectionStatus) {
|
||||
case JitsiParticipantConnectionStatus.INTERRUPTED:
|
||||
tipKey = 'connectionindicator.quality.lost';
|
||||
break;
|
||||
|
||||
case JitsiParticipantConnectionStatus.INACTIVE:
|
||||
tipKey = 'connectionindicator.quality.inactive';
|
||||
break;
|
||||
|
||||
default: {
|
||||
const { percent } = this.state.stats;
|
||||
|
||||
if (typeof percent === 'undefined') {
|
||||
// If percentage is undefined then there are no stats available
|
||||
// yet, likely because only a local connection has been
|
||||
// established so far. Assume a strong connection to start.
|
||||
tipKey = 'connectionindicator.quality.good';
|
||||
} else {
|
||||
const config = this._getDisplayConfiguration(percent);
|
||||
|
||||
tipKey = config.tip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.props.t(tipKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon configuration from QUALITY_TO_WIDTH which has a percentage
|
||||
* that matches or exceeds the passed in percentage. The implementation
|
||||
|
@ -297,19 +220,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
|||
? 'show-connection-indicator' : 'hide-connection-indicator';
|
||||
}
|
||||
|
||||
_onToggleShowMore: () => void;
|
||||
|
||||
/**
|
||||
* Callback to invoke when the show more link in the popover content is
|
||||
* clicked. Sets the state which will determine if the popover should show
|
||||
* additional statistics about the connection.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleShowMore() {
|
||||
this.setState({ showMoreStats: !this.state.showMoreStats });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ReactElement for displaying an icon that represents the current
|
||||
* connection quality.
|
||||
|
@ -367,80 +277,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
|||
</span>
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code ConnectionStatisticsTable} instance.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderStatisticsTable() {
|
||||
const {
|
||||
bandwidth,
|
||||
bitrate,
|
||||
bridgeCount,
|
||||
codec,
|
||||
e2eRtt,
|
||||
framerate,
|
||||
maxEnabledResolution,
|
||||
packetLoss,
|
||||
region,
|
||||
resolution,
|
||||
serverRegion,
|
||||
transport
|
||||
} = this.state.stats;
|
||||
|
||||
return (
|
||||
<ConnectionStatsTable
|
||||
audioSsrc = { this.props.audioSsrc }
|
||||
bandwidth = { bandwidth }
|
||||
bitrate = { bitrate }
|
||||
bridgeCount = { bridgeCount }
|
||||
codec = { codec }
|
||||
connectionSummary = { this._getConnectionStatusTip() }
|
||||
disableShowMoreStats = { this.props.disableShowMoreStats }
|
||||
e2eRtt = { e2eRtt }
|
||||
enableSaveLogs = { this.props.enableSaveLogs }
|
||||
framerate = { framerate }
|
||||
isLocalVideo = { this.props.isLocalVideo }
|
||||
maxEnabledResolution = { maxEnabledResolution }
|
||||
onSaveLogs = { this.props._onSaveLogs }
|
||||
onShowMore = { this._onToggleShowMore }
|
||||
packetLoss = { packetLoss }
|
||||
participantId = { this.props.participantId }
|
||||
region = { region }
|
||||
resolution = { resolution }
|
||||
serverRegion = { serverRegion }
|
||||
shouldShowMore = { this.state.showMoreStats }
|
||||
transport = { transport }
|
||||
videoSsrc = { this.props.videoSsrc } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps redux actions to the props of the component.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* _onSaveLogs: Function,
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
export function _mapDispatchToProps(dispatch: Dispatch<any>) {
|
||||
return {
|
||||
/**
|
||||
* Saves the conference logs.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onSaveLogs() {
|
||||
dispatch(saveLogs());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
|
@ -450,30 +288,11 @@ export function _mapDispatchToProps(dispatch: Dispatch<any>) {
|
|||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { participantId } = ownProps;
|
||||
const conference = state['features/base/conference'].conference;
|
||||
const participant
|
||||
= typeof participantId === 'undefined' ? getLocalParticipant(state) : getParticipantById(state, participantId);
|
||||
const props = {
|
||||
_connectionStatus: participant?.connectionStatus,
|
||||
enableSaveLogs: state['features/base/config'].enableSaveLogs,
|
||||
disableShowMoreStats: state['features/base/config'].disableShowMoreStats
|
||||
};
|
||||
|
||||
if (conference) {
|
||||
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
|
||||
const firstAudioTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantId);
|
||||
|
||||
return {
|
||||
...props,
|
||||
audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
|
||||
videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
|
||||
};
|
||||
}
|
||||
= participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
...props
|
||||
_connectionStatus: participant?.connectionStatus
|
||||
};
|
||||
}
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ConnectionIndicator));
|
||||
export default translate(connect(_mapStateToProps)(ConnectionIndicator));
|
||||
|
|
|
@ -0,0 +1,325 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
||||
import { ConnectionStatsTable } from '../../../connection-stats';
|
||||
import { saveLogs } from '../../actions';
|
||||
import AbstractConnectionIndicator, {
|
||||
INDICATOR_DISPLAY_THRESHOLD,
|
||||
type Props as AbstractProps,
|
||||
type State as AbstractState
|
||||
} from '../AbstractConnectionIndicator';
|
||||
|
||||
/**
|
||||
* An array of display configurations for the connection indicator and its bars.
|
||||
* The ordering is done specifically for faster iteration to find a matching
|
||||
* configuration to the current connection strength percentage.
|
||||
*
|
||||
* @type {Object[]}
|
||||
*/
|
||||
const QUALITY_TO_WIDTH: Array<Object> = [
|
||||
|
||||
// Full (3 bars)
|
||||
{
|
||||
colorClass: 'status-high',
|
||||
percent: INDICATOR_DISPLAY_THRESHOLD,
|
||||
tip: 'connectionindicator.quality.good',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
// 2 bars
|
||||
{
|
||||
colorClass: 'status-med',
|
||||
percent: 10,
|
||||
tip: 'connectionindicator.quality.nonoptimal',
|
||||
width: '66%'
|
||||
},
|
||||
|
||||
// 1 bar
|
||||
{
|
||||
colorClass: 'status-low',
|
||||
percent: 0,
|
||||
tip: 'connectionindicator.quality.poor',
|
||||
width: '33%'
|
||||
}
|
||||
|
||||
// Note: we never show 0 bars as long as there is a connection.
|
||||
];
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ConnectionIndicator}.
|
||||
*/
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* The current condition of the user's connection, matching one of the
|
||||
* enumerated values in the library.
|
||||
*/
|
||||
_connectionStatus: string,
|
||||
|
||||
/**
|
||||
* The audio SSRC of this client.
|
||||
*/
|
||||
audioSsrc: number,
|
||||
|
||||
/**
|
||||
* Css class to apply on container
|
||||
*/
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Whether or not should display the "Show More" link in the local video
|
||||
* stats table.
|
||||
*/
|
||||
disableShowMoreStats: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not should display the "Save Logs" link in the local video
|
||||
* stats table.
|
||||
*/
|
||||
enableSaveLogs: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the displays stats are for local video.
|
||||
*/
|
||||
isLocalVideo: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The video SSRC of this client.
|
||||
*/
|
||||
videoSsrc: number,
|
||||
|
||||
/**
|
||||
* Invoked to save the conference logs.
|
||||
*/
|
||||
_onSaveLogs: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link ConnectionIndicator}.
|
||||
*/
|
||||
type State = AbstractState & {
|
||||
|
||||
/**
|
||||
* Whether or not the popover content should display additional statistics.
|
||||
*/
|
||||
showMoreStats: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays the current connection
|
||||
* quality percentage and has a popover to show more detailed connection stats.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code ConnectionIndicator} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
autoHideTimeout: undefined,
|
||||
showIndicator: false,
|
||||
showMoreStats: false,
|
||||
stats: {}
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onToggleShowMore = this._onToggleShowMore.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
bandwidth,
|
||||
bitrate,
|
||||
bridgeCount,
|
||||
codec,
|
||||
e2eRtt,
|
||||
framerate,
|
||||
maxEnabledResolution,
|
||||
packetLoss,
|
||||
region,
|
||||
resolution,
|
||||
serverRegion,
|
||||
transport
|
||||
} = this.state.stats;
|
||||
|
||||
return (
|
||||
<ConnectionStatsTable
|
||||
audioSsrc = { this.props.audioSsrc }
|
||||
bandwidth = { bandwidth }
|
||||
bitrate = { bitrate }
|
||||
bridgeCount = { bridgeCount }
|
||||
codec = { codec }
|
||||
connectionSummary = { this._getConnectionStatusTip() }
|
||||
disableShowMoreStats = { this.props.disableShowMoreStats }
|
||||
e2eRtt = { e2eRtt }
|
||||
enableSaveLogs = { this.props.enableSaveLogs }
|
||||
framerate = { framerate }
|
||||
isLocalVideo = { this.props.isLocalVideo }
|
||||
maxEnabledResolution = { maxEnabledResolution }
|
||||
onSaveLogs = { this.props._onSaveLogs }
|
||||
onShowMore = { this._onToggleShowMore }
|
||||
packetLoss = { packetLoss }
|
||||
participantId = { this.props.participantId }
|
||||
region = { region }
|
||||
resolution = { resolution }
|
||||
serverRegion = { serverRegion }
|
||||
shouldShowMore = { this.state.showMoreStats }
|
||||
transport = { transport }
|
||||
videoSsrc = { this.props.videoSsrc } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string that describes the current connection status.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getConnectionStatusTip() {
|
||||
let tipKey;
|
||||
|
||||
switch (this.props._connectionStatus) {
|
||||
case JitsiParticipantConnectionStatus.INTERRUPTED:
|
||||
tipKey = 'connectionindicator.quality.lost';
|
||||
break;
|
||||
|
||||
case JitsiParticipantConnectionStatus.INACTIVE:
|
||||
tipKey = 'connectionindicator.quality.inactive';
|
||||
break;
|
||||
|
||||
default: {
|
||||
const { percent } = this.state.stats;
|
||||
|
||||
if (typeof percent === 'undefined') {
|
||||
// If percentage is undefined then there are no stats available
|
||||
// yet, likely because only a local connection has been
|
||||
// established so far. Assume a strong connection to start.
|
||||
tipKey = 'connectionindicator.quality.good';
|
||||
} else {
|
||||
const config = this._getDisplayConfiguration(percent);
|
||||
|
||||
tipKey = config.tip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.props.t(tipKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon configuration from QUALITY_TO_WIDTH which has a percentage
|
||||
* that matches or exceeds the passed in percentage. The implementation
|
||||
* assumes QUALITY_TO_WIDTH is already sorted by highest to lowest
|
||||
* percentage.
|
||||
*
|
||||
* @param {number} percent - The connection percentage, out of 100, to find
|
||||
* the closest matching configuration for.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getDisplayConfiguration(percent: number): Object {
|
||||
return QUALITY_TO_WIDTH.find(x => percent >= x.percent) || {};
|
||||
}
|
||||
|
||||
|
||||
_onToggleShowMore: () => void;
|
||||
|
||||
/**
|
||||
* Callback to invoke when the show more link in the popover content is
|
||||
* clicked. Sets the state which will determine if the popover should show
|
||||
* additional statistics about the connection.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleShowMore() {
|
||||
this.setState({ showMoreStats: !this.state.showMoreStats });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux actions to the props of the component.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* _onSaveLogs: Function,
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
export function _mapDispatchToProps(dispatch: Dispatch<any>) {
|
||||
return {
|
||||
/**
|
||||
* Saves the conference logs.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onSaveLogs() {
|
||||
dispatch(saveLogs());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Props} ownProps - The own props of the component.
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { participantId } = ownProps;
|
||||
const conference = state['features/base/conference'].conference;
|
||||
const participant
|
||||
= participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
|
||||
const props = {
|
||||
_connectionStatus: participant?.connectionStatus,
|
||||
enableSaveLogs: state['features/base/config'].enableSaveLogs,
|
||||
disableShowMoreStats: state['features/base/config'].disableShowMoreStats
|
||||
};
|
||||
|
||||
if (conference) {
|
||||
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
|
||||
const firstAudioTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantId);
|
||||
|
||||
return {
|
||||
...props,
|
||||
audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
|
||||
videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ConnectionIndicatorContent));
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isMobileBrowser } from '../../../features/base/environment/utils';
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
/**
|
||||
|
@ -176,10 +177,11 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
*/
|
||||
render() {
|
||||
const { isLocalVideo, enableSaveLogs, disableShowMoreStats } = this.props;
|
||||
const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'connection-info'
|
||||
className = { className }
|
||||
onClick = { onClick }>
|
||||
{ this._renderStatistics() }
|
||||
<div className = 'connection-actions'>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isMobileBrowser } from '../../../../../react/features/base/environment/utils';
|
||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
|
||||
import { AudioLevelIndicator } from '../../../audio-level-indicator';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
|
@ -33,7 +34,8 @@ import {
|
|||
DISPLAY_MODE_TO_STRING,
|
||||
DISPLAY_VIDEO,
|
||||
DISPLAY_VIDEO_WITH_NAME,
|
||||
VIDEO_TEST_EVENTS
|
||||
VIDEO_TEST_EVENTS,
|
||||
SHOW_TOOLBAR_CONTEXT_MENU_AFTER
|
||||
} from '../../constants';
|
||||
import { isVideoPlayable, computeDisplayMode } from '../../functions';
|
||||
import logger from '../../logger';
|
||||
|
@ -237,6 +239,16 @@ function onClick(event) {
|
|||
* @extends Component
|
||||
*/
|
||||
class Thumbnail extends Component<Props, State> {
|
||||
/**
|
||||
* The long touch setTimeout handler.
|
||||
*/
|
||||
timeoutHandle: Object;
|
||||
|
||||
/**
|
||||
* Reference to local or remote Video Menu trigger button instance.
|
||||
*/
|
||||
videoMenuTriggerRef: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new Thumbnail instance.
|
||||
*
|
||||
|
@ -257,7 +269,10 @@ class Thumbnail extends Component<Props, State> {
|
|||
...state,
|
||||
displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state))
|
||||
};
|
||||
this.timeoutHandle = null;
|
||||
this.videoMenuTriggerRef = null;
|
||||
|
||||
this._setInstance = this._setInstance.bind(this);
|
||||
this._updateAudioLevel = this._updateAudioLevel.bind(this);
|
||||
this._onCanPlay = this._onCanPlay.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
|
@ -265,6 +280,10 @@ class Thumbnail extends Component<Props, State> {
|
|||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onTestingEvent = this._onTestingEvent.bind(this);
|
||||
this._onTouchStart = this._onTouchStart.bind(this);
|
||||
this._onTouchEnd = this._onTouchEnd.bind(this);
|
||||
this._onTouchMove = this._onTouchMove.bind(this);
|
||||
this._showPopupMenu = this._showPopupMenu.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -539,6 +558,54 @@ class Thumbnail extends Component<Props, State> {
|
|||
this.setState({ isHovered: false });
|
||||
}
|
||||
|
||||
_showPopupMenu: () => void;
|
||||
|
||||
/**
|
||||
* Triggers showing the popover context menu.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_showPopupMenu() {
|
||||
if (this.videoMenuTriggerRef) {
|
||||
this.videoMenuTriggerRef.showContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
_onTouchStart: () => void;
|
||||
|
||||
/**
|
||||
* Set showing popover context menu after x miliseconds.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTouchStart() {
|
||||
this.timeoutHandle = setTimeout(this._showPopupMenu, SHOW_TOOLBAR_CONTEXT_MENU_AFTER);
|
||||
}
|
||||
|
||||
_onTouchEnd: () => void;
|
||||
|
||||
/**
|
||||
* Cancel showing popover context menu after x miliseconds if the no. Of miliseconds is not reached yet,
|
||||
* or just clears the timeout.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTouchEnd() {
|
||||
clearTimeout(this.timeoutHandle);
|
||||
}
|
||||
|
||||
_onTouchMove: () => void;
|
||||
|
||||
/**
|
||||
* Cancel showing Context menu after x miliseconds if the number of miliseconds is not reached
|
||||
* before a touch move(drag), or just clears the timeout.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTouchMove() {
|
||||
clearTimeout(this.timeoutHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a fake participant (youtube video) thumbnail.
|
||||
*
|
||||
|
@ -709,6 +776,11 @@ class Thumbnail extends Component<Props, State> {
|
|||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
{ ...(isMobileBrowser() ? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
onTouchMove: this._onTouchMove,
|
||||
onTouchStart: this._onTouchStart
|
||||
} : {}) }
|
||||
style = { styles.thumbnail }>
|
||||
<div className = 'videocontainer__background' />
|
||||
<span id = 'localVideoWrapper'>
|
||||
|
@ -738,8 +810,10 @@ class Thumbnail extends Component<Props, State> {
|
|||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||
</span>
|
||||
<span className = 'localvideomenu'>
|
||||
<LocalVideoMenuTriggerButton />
|
||||
<LocalVideoMenuTriggerButton
|
||||
getRef = { this._setInstance } />
|
||||
</span>
|
||||
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -783,6 +857,19 @@ class Thumbnail extends Component<Props, State> {
|
|||
dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
|
||||
}
|
||||
|
||||
_setInstance: Object => void;
|
||||
|
||||
/**
|
||||
* Stores the local or remote video menu button instance in a variable.
|
||||
*
|
||||
* @param {Object} instance - The local or remote video menu trigger instance.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_setInstance(instance) {
|
||||
this.videoMenuTriggerRef = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a remote participant's 'thumbnail.
|
||||
*
|
||||
|
@ -826,6 +913,11 @@ class Thumbnail extends Component<Props, State> {
|
|||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
{ ...(isMobileBrowser() ? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
onTouchMove: this._onTouchMove,
|
||||
onTouchStart: this._onTouchStart
|
||||
} : {}) }
|
||||
style = { styles.thumbnail }>
|
||||
{
|
||||
_videoTrack && <VideoTrack
|
||||
|
@ -859,6 +951,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
</span>
|
||||
<span className = 'remotevideomenu'>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
getRef = { this._setInstance }
|
||||
initialVolumeValue = { _volume }
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { id } />
|
||||
|
@ -982,7 +1075,7 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
return {
|
||||
_audioTrack,
|
||||
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
|
||||
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||
_connectionIndicatorDisabled: isMobileBrowser() || interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||
_currentLayout,
|
||||
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
||||
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
|
||||
|
|
|
@ -208,3 +208,10 @@ export const VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN = 10;
|
|||
* @type {number}
|
||||
*/
|
||||
export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
|
||||
|
||||
/**
|
||||
* Sets after how many ms to show the thumbnail context menu on long touch on mobile.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
|
||||
|
|
|
@ -48,22 +48,24 @@ function Drawer({
|
|||
const drawerRef: Object = useRef(null);
|
||||
|
||||
/**
|
||||
* Closes the drawer when clicking outside of it.
|
||||
* Closes the drawer when clicking or touching outside of it.
|
||||
*
|
||||
* @param {Event} event - Mouse down event object.
|
||||
* @param {Event} event - Mouse down/start touch event object.
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleOutsideClick(event: MouseEvent) {
|
||||
function handleOutsideClickOrTouch(event: Event) {
|
||||
if (drawerRef.current && !drawerRef.current.contains(event.target)) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousedown', handleOutsideClick);
|
||||
window.addEventListener('mousedown', handleOutsideClickOrTouch);
|
||||
window.addEventListener('touchstart', handleOutsideClickOrTouch);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handleOutsideClick);
|
||||
window.removeEventListener('mousedown', handleOutsideClickOrTouch);
|
||||
window.removeEventListener('touchstart', handleOutsideClickOrTouch);
|
||||
};
|
||||
}, [ drawerRef ]);
|
||||
|
||||
|
|
|
@ -1,2 +1,18 @@
|
|||
// @flow
|
||||
import { SHOW_CONNECTION_INFO } from '../base/connection/actionTypes';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Sets whether to render the connnection status info into the Popover of the thumbnail or the context menu buttons.
|
||||
*
|
||||
* @param {boolean} showConnectionInfo - Whether it should show the connection
|
||||
* info or the context menu buttons on thumbnail popover.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function renderConnectionStatus(showConnectionInfo: boolean) {
|
||||
return {
|
||||
type: SHOW_CONNECTION_INFO,
|
||||
showConnectionInfo
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// @flow
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconInfo } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The ID of the participant for which to show connection stats.
|
||||
*/
|
||||
participantId: string,
|
||||
|
||||
/**
|
||||
* The function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
|
||||
const ConnectionStatusButton = ({
|
||||
dispatch,
|
||||
participantId,
|
||||
t
|
||||
}: Props) => {
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(renderConnectionStatus(true));
|
||||
}, [ dispatch ]);
|
||||
|
||||
return (
|
||||
<VideoMenuButton
|
||||
buttonText = { t('videothumbnail.connectionInfo') }
|
||||
icon = { IconInfo }
|
||||
id = { `connstatus_${participantId}` }
|
||||
onClick = { onClick } />
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(connect()(ConnectionStatusButton));
|
|
@ -1,23 +1,46 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconMenuThumb } from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant
|
||||
} from '../../../base/participants';
|
||||
import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getLocalVideoTrack } from '../../../base/tracks';
|
||||
import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import FlipLocalVideoButton from './FlipLocalVideoButton';
|
||||
import VideoMenu from './VideoMenu';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link LocalVideoMenuTriggerButton}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Gets a ref to the current component instance.
|
||||
*/
|
||||
getRef: Function,
|
||||
|
||||
/**
|
||||
* The id of the local participant.
|
||||
*/
|
||||
_localParticipantId: string,
|
||||
|
||||
/**
|
||||
* The position relative to the trigger the local video menu should display
|
||||
* from. Valid values are those supported by AtlasKit
|
||||
|
@ -30,6 +53,11 @@ type Props = {
|
|||
*/
|
||||
_overflowDrawer: boolean,
|
||||
|
||||
/**
|
||||
* Whether to render the connection info pane.
|
||||
*/
|
||||
_showConnectionInfo: boolean,
|
||||
|
||||
/**
|
||||
* Shows/hides the local video flip button.
|
||||
*/
|
||||
|
@ -45,33 +73,124 @@ type Props = {
|
|||
* React Component for displaying an icon associated with opening the
|
||||
* the video menu for the local participant.
|
||||
*
|
||||
* @param {Props} props - The props passed to the component.
|
||||
* @returns {ReactElement}
|
||||
* @extends {Component}
|
||||
*/
|
||||
function LocalVideoMenuTriggerButton(props: Props) {
|
||||
return (
|
||||
props._showLocalVideoFlipButton
|
||||
? <Popover
|
||||
content = {
|
||||
<VideoMenu id = 'localVideoMenu'>
|
||||
<FlipLocalVideoButton />
|
||||
</VideoMenu>
|
||||
}
|
||||
overflowDrawer = { props._overflowDrawer }
|
||||
position = { props._menuPosition }>
|
||||
<span
|
||||
className = 'popover-trigger local-video-menu-trigger'>
|
||||
<Icon
|
||||
ariaLabel = { props.t('dialog.localUserControls') }
|
||||
role = 'button'
|
||||
size = '1em'
|
||||
src = { IconMenuThumb }
|
||||
tabIndex = { 0 }
|
||||
title = { props.t('dialog.localUserControls') } />
|
||||
</span>
|
||||
</Popover>
|
||||
: null
|
||||
);
|
||||
class LocalVideoMenuTriggerButton extends Component<Props> {
|
||||
/**
|
||||
* Reference to the Popover instance.
|
||||
*/
|
||||
popoverRef: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new LocalVideoMenuTriggerButton instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.popoverRef = React.createRef();
|
||||
this._onPopoverClose = this._onPopoverClose.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers showing the popover's context menu.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
showContextMenu() {
|
||||
if (this.popoverRef && this.popoverRef.current) {
|
||||
this.popoverRef.current.showDialog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the ref(instance) getter.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.props.getRef) {
|
||||
this.props.getRef(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the ref(instance) getter.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this.props.getRef) {
|
||||
this.props.getRef(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_localParticipantId,
|
||||
_menuPosition,
|
||||
_showConnectionInfo,
|
||||
_overflowDrawer,
|
||||
_showLocalVideoFlipButton,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const content = _showConnectionInfo
|
||||
? <ConnectionIndicatorContent participantId = { _localParticipantId } />
|
||||
: (
|
||||
<VideoMenu id = 'localVideoMenu'>
|
||||
<FlipLocalVideoButton />
|
||||
{ isMobileBrowser()
|
||||
&& <ConnectionStatusButton participantId = { _localParticipantId } />
|
||||
}
|
||||
</VideoMenu>
|
||||
);
|
||||
|
||||
return (
|
||||
isMobileBrowser() || _showLocalVideoFlipButton
|
||||
? <Popover
|
||||
content = { content }
|
||||
onPopoverClose = { this._onPopoverClose }
|
||||
overflowDrawer = { _overflowDrawer }
|
||||
position = { _menuPosition }
|
||||
ref = { this.popoverRef }>
|
||||
{!isMobileBrowser() && (
|
||||
<span
|
||||
className = 'popover-trigger local-video-menu-trigger'>
|
||||
<Icon
|
||||
ariaLabel = { t('dialog.localUserControls') }
|
||||
role = 'button'
|
||||
size = '1em'
|
||||
src = { IconMenuThumb }
|
||||
tabIndex = { 0 }
|
||||
title = { t('dialog.localUserControls') } />
|
||||
</span>
|
||||
)}
|
||||
</Popover>
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
_onPopoverClose: () => void;
|
||||
|
||||
/**
|
||||
* Render normal context menu next time popover dialog opens.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPopoverClose() {
|
||||
this.props.dispatch(renderConnectionStatus(false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,9 +202,12 @@ function LocalVideoMenuTriggerButton(props: Props) {
|
|||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { disableLocalVideoFlip } = state['features/base/config'];
|
||||
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
const { overflowDrawer } = state['features/toolbox'];
|
||||
const { showConnectionInfo } = state['features/base/connection'];
|
||||
|
||||
let _menuPosition;
|
||||
|
||||
switch (currentLayout) {
|
||||
|
@ -102,7 +224,9 @@ function _mapStateToProps(state) {
|
|||
return {
|
||||
_menuPosition,
|
||||
_showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop',
|
||||
_overflowDrawer: overflowDrawer
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_localParticipantId: localParticipant.id,
|
||||
_showConnectionInfo: showConnectionInfo
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import ConnectionIndicatorContent from
|
||||
'../../../../features/connection-indicator/components/web/ConnectionIndicatorContent';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconMenuThumb } from '../../../base/icons';
|
||||
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
|
||||
|
@ -9,11 +12,14 @@ import { Popover } from '../../../base/popover';
|
|||
import { connect } from '../../../base/redux';
|
||||
import { requestRemoteControl, stopController } from '../../../remote-control';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
|
||||
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
|
||||
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
|
||||
|
||||
|
||||
import {
|
||||
GrantModeratorButton,
|
||||
MuteButton,
|
||||
|
@ -26,7 +32,6 @@ import {
|
|||
} from './';
|
||||
|
||||
declare var $: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
|
@ -71,12 +76,16 @@ type Props = {
|
|||
*/
|
||||
_remoteControlState: number,
|
||||
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Gets a ref to the current component instance.
|
||||
*/
|
||||
getRef: Function,
|
||||
|
||||
/**
|
||||
* A value between 0 and 1 indicating the volume of the participant's
|
||||
* audio element.
|
||||
|
@ -99,6 +108,11 @@ type Props = {
|
|||
*/
|
||||
_participantDisplayName: string,
|
||||
|
||||
/**
|
||||
* Whether the popover should render the Connection Info stats.
|
||||
*/
|
||||
_showConnectionInfo: Boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
|
@ -112,6 +126,59 @@ type Props = {
|
|||
* @extends {Component}
|
||||
*/
|
||||
class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
/**
|
||||
* Reference to the Popover instance.
|
||||
*/
|
||||
popoverRef: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new RemoteVideoMenuTriggerButton instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.popoverRef = React.createRef();
|
||||
this._onPopoverClose = this._onPopoverClose.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers showing the popover's context menu.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
showContextMenu() {
|
||||
if (this.popoverRef && this.popoverRef.current) {
|
||||
this.popoverRef.current.showDialog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the ref(instance) getter.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.props.getRef) {
|
||||
this.props.getRef(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the ref(instance) getter.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this.props.getRef) {
|
||||
this.props.getRef(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -119,32 +186,50 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const content = this._renderRemoteVideoMenu();
|
||||
const { _showConnectionInfo, _participantDisplayName, participantID } = this.props;
|
||||
const content = _showConnectionInfo
|
||||
? <ConnectionIndicatorContent participantId = { participantID } />
|
||||
: this._renderRemoteVideoMenu();
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = this.props._participantDisplayName;
|
||||
const username = _participantDisplayName;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content = { content }
|
||||
onPopoverClose = { this._onPopoverClose }
|
||||
overflowDrawer = { this.props._overflowDrawer }
|
||||
position = { this.props._menuPosition }>
|
||||
<span className = 'popover-trigger remote-video-menu-trigger'>
|
||||
<Icon
|
||||
ariaLabel = { this.props.t('dialog.remoteUserControls', { username }) }
|
||||
role = 'button'
|
||||
size = '1.4em'
|
||||
src = { IconMenuThumb }
|
||||
tabIndex = { 0 }
|
||||
title = { this.props.t('dialog.remoteUserControls', { username }) } />
|
||||
</span>
|
||||
position = { this.props._menuPosition }
|
||||
ref = { this.popoverRef }>
|
||||
{!isMobileBrowser() && (
|
||||
<span className = 'popover-trigger remote-video-menu-trigger'>
|
||||
<Icon
|
||||
ariaLabel = { this.props.t('dialog.remoteUserControls', { username }) }
|
||||
role = 'button'
|
||||
size = '1.4em'
|
||||
src = { IconMenuThumb }
|
||||
tabIndex = { 0 }
|
||||
title = { this.props.t('dialog.remoteUserControls', { username }) } />
|
||||
</span>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
_onPopoverClose: () => void;
|
||||
|
||||
/**
|
||||
* Render normal context menu next time popover dialog opens.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPopoverClose() {
|
||||
this.props.dispatch(renderConnectionStatus(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code VideoMenu} with buttons for interacting with
|
||||
* the remote participant.
|
||||
|
@ -232,6 +317,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
participantID = { participantID } />
|
||||
);
|
||||
|
||||
if (isMobileBrowser()) {
|
||||
buttons.push(
|
||||
<ConnectionStatusButton
|
||||
participantId = { participantID } />
|
||||
);
|
||||
}
|
||||
|
||||
if (onVolumeChange && typeof initialVolumeValue === 'number' && !isNaN(initialVolumeValue)) {
|
||||
buttons.push(
|
||||
|
@ -276,6 +367,7 @@ function _mapStateToProps(state, ownProps) {
|
|||
const { requestedParticipant, controlled } = controller;
|
||||
const activeParticipant = requestedParticipant || controlled;
|
||||
const { overflowDrawer } = state['features/toolbox'];
|
||||
const { showConnectionInfo } = state['features/base/connection'];
|
||||
|
||||
if (_supportsRemoteControl
|
||||
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
|
||||
|
@ -310,7 +402,8 @@ function _mapStateToProps(state, ownProps) {
|
|||
_menuPosition,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_participantDisplayName,
|
||||
_disableGrantModerator: Boolean(disableGrantModerator)
|
||||
_disableGrantModerator: Boolean(disableGrantModerator),
|
||||
_showConnectionInfo: showConnectionInfo
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
export { default as ConnectionStatusButton } from './ConnectionStatusButton';
|
||||
export { default as GrantModeratorButton } from './GrantModeratorButton';
|
||||
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
|
||||
export { default as KickButton } from './KickButton';
|
||||
|
|
Loading…
Reference in New Issue