// @flow import React from 'react'; 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 { getLocalParticipant, getParticipantById } from '../../../base/participants'; import { Popover } from '../../../base/popover'; import { connect } from '../../../base/redux'; import AbstractConnectionIndicator, { INDICATOR_DISPLAY_THRESHOLD, type Props as AbstractProps, type State as AbstractState } from '../AbstractConnectionIndicator'; import ConnectionIndicatorContent from './ConnectionIndicatorContent'; /** * 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 = [ // 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, /** * Whether or not the component should ignore setting a visibility class for * hiding the component when the connection quality is not strong. */ alwaysVisible: boolean, /** * The audio SSRC of this client. */ audioSsrc: number, /** * The Redux dispatch function. */ dispatch: Dispatch, /** * Whether or not clicking the indicator should display a popover for more * details. */ enableStatsDisplay: boolean, /** * The font-size for the icon. */ iconSize: number, /** * Whether or not the displays stats are for local video. */ isLocalVideo: boolean, /** * Relative to the icon from where the popover for more connection details * should display. */ statsPopoverPosition: string, /** * Invoked to obtain translated strings. */ t: Function, }; /** * 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 ConnectionIndicator extends AbstractConnectionIndicator { /** * 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, stats: {} }; } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { const visibilityClass = this._getVisibilityClass(); const rootClassNames = `indicator-container ${visibilityClass}`; const colorClass = this._getConnectionColorClass(); const indicatorContainerClassNames = `connection-indicator indicator ${colorClass}`; return ( } disablePopover = { !this.props.enableStatsDisplay } position = { this.props.statsPopoverPosition }>
{ this._renderIcon() }
); } /** * Returns a CSS class that interprets the current connection status as a * color. * * @private * @returns {string} */ _getConnectionColorClass() { const { _connectionStatus } = this.props; const { percent } = this.state.stats; const { INACTIVE, INTERRUPTED } = JitsiParticipantConnectionStatus; if (_connectionStatus === INACTIVE) { return 'status-other'; } else if (_connectionStatus === INTERRUPTED) { return 'status-lost'; } else if (typeof percent === 'undefined') { return 'status-high'; } return this._getDisplayConfiguration(percent).colorClass; } /** * 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) || {}; } /** * Returns additional class names to add to the root of the component. The * class names are intended to be used for hiding or showing the indicator. * * @private * @returns {string} */ _getVisibilityClass() { const { _connectionStatus } = this.props; return this.state.showIndicator || this.props.alwaysVisible || _connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED || _connectionStatus === JitsiParticipantConnectionStatus.INACTIVE ? 'show-connection-indicator' : 'hide-connection-indicator'; } /** * Creates a ReactElement for displaying an icon that represents the current * connection quality. * * @returns {ReactElement} */ _renderIcon() { if (this.props._connectionStatus === JitsiParticipantConnectionStatus.INACTIVE) { return ( ); } let iconWidth; let emptyIconWrapperClassName = 'connection_empty'; if (this.props._connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED) { // emptyIconWrapperClassName is used by the torture tests to // identify lost connection status handling. emptyIconWrapperClassName = 'connection_lost'; iconWidth = '0%'; } else if (typeof this.state.stats.percent === 'undefined') { iconWidth = '100%'; } else { const { percent } = this.state.stats; iconWidth = this._getDisplayConfiguration(percent).width; } return [ , ]; } } /** * 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 participant = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state); return { _connectionStatus: participant?.connectionStatus }; } export default translate(connect(_mapStateToProps)(ConnectionIndicator));