/* eslint-disable lines-around-comment */ import { Theme } from '@mui/material'; import { withStyles } from '@mui/styles'; import clsx from 'clsx'; import React from 'react'; import { WithTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import type { Dispatch } from 'redux'; import { IState } from '../../../app/types'; // @ts-ignore import { getSourceNameSignalingFeatureFlag } from '../../../base/config'; import { translate } from '../../../base/i18n/functions'; import { MEDIA_TYPE } from '../../../base/media/constants'; import { getLocalParticipant, getParticipantById } from '../../../base/participants/functions'; // @ts-ignore import { Popover } from '../../../base/popover'; import { getVirtualScreenshareParticipantTrack, getSourceNameByParticipantId, // @ts-ignore getTrackByMediaTypeAndParticipant } from '../../../base/tracks'; import { isParticipantConnectionStatusInactive, isParticipantConnectionStatusInterrupted, isTrackStreamingStatusInactive, isTrackStreamingStatusInterrupted // @ts-ignore } from '../../functions'; import AbstractConnectionIndicator, { INDICATOR_DISPLAY_THRESHOLD, type Props as AbstractProps, type State as AbstractState // @ts-ignore } from '../AbstractConnectionIndicator'; // @ts-ignore import ConnectionIndicatorContent from './ConnectionIndicatorContent'; // @ts-ignore import { ConnectionIndicatorIcon } from './ConnectionIndicatorIcon'; /** * 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<{ colorClass: string; percent: number; tip: string; }> = [ // Full (3 bars) { colorClass: 'status-high', percent: INDICATOR_DISPLAY_THRESHOLD, tip: 'connectionindicator.quality.good' }, // 2 bars { colorClass: 'status-med', percent: 10, tip: 'connectionindicator.quality.nonoptimal' }, // 1 bar { colorClass: 'status-low', percent: 0, tip: 'connectionindicator.quality.poor' } // 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 & WithTranslation & { /** * Disable/enable inactive indicator. */ _connectionIndicatorInactiveDisabled: boolean; /** * The current condition of the user's connection, matching one of the * enumerated values in the library. */ _connectionStatus: string; /** * Whether the indicator popover is disabled. */ _popoverDisabled: boolean; /** * The source name of the track. */ _sourceName: string; /** * Whether source name signaling is enabled. */ _sourceNameSignalingEnabled: boolean; /** * 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; /** * An object containing the CSS classes. */ classes: any; /** * 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; /** * Relative to the icon from where the popover for more connection details * should display. */ statsPopoverPosition: string; }; type State = AbstractState & { /** * Whether popover is ivisible or not. */ popoverVisible: boolean; }; const styles = (theme: Theme) => { return { container: { display: 'inline-block' }, hidden: { display: 'none' }, icon: { padding: '6px', borderRadius: '4px', '&.status-high': { backgroundColor: theme.palette.success01 }, '&.status-med': { backgroundColor: theme.palette.warning01 }, '&.status-low': { backgroundColor: theme.palette.iconError }, '&.status-disabled': { background: 'transparent' }, '&.status-lost': { backgroundColor: theme.palette.ui05 }, '&.status-other': { backgroundColor: theme.palette.action01 } }, inactiveIcon: { padding: 0, borderRadius: '50%' } }; }; /** * Implements a React {@link Component} which displays the current connection * quality percentage and has a popover to show more detailed connection stats. * * @augments {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); // @ts-ignore this.state = { showIndicator: false, stats: {}, popoverVisible: false }; this._onShowPopover = this._onShowPopover.bind(this); this._onHidePopover = this._onHidePopover.bind(this); } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { // @ts-ignore const { enableStatsDisplay, participantId, statsPopoverPosition, classes } = this.props; const visibilityClass = this._getVisibilityClass(); // @ts-ignore if (this.props._popoverDisabled) { return this._renderIndicator(); } return ( } disablePopover = { !enableStatsDisplay } id = 'participant-connection-indicator' noPaddingContent = { true } onPopoverClose = { this._onHidePopover } onPopoverOpen = { this._onShowPopover } position = { statsPopoverPosition } // @ts-ignore visible = { this.state.popoverVisible }> { this._renderIndicator() } ); } /** * Returns a CSS class that interprets the current connection status as a * color. * * @private * @returns {string} */ _getConnectionColorClass() { // TODO We currently do not have logic to emit and handle stats changes for tracks. // @ts-ignore const { percent } = this.state.stats; const { _isConnectionStatusInactive, _isConnectionStatusInterrupted, _connectionIndicatorInactiveDisabled // @ts-ignore } = this.props; if (_isConnectionStatusInactive) { if (_connectionIndicatorInactiveDisabled) { return 'status-disabled'; } return 'status-other'; } else if (_isConnectionStatusInterrupted) { 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): any { 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() { // @ts-ignore const { _isConnectionStatusInactive, _isConnectionStatusInterrupted, classes } = this.props; // @ts-ignore return this.state.showIndicator // @ts-ignore || this.props.alwaysVisible || _isConnectionStatusInterrupted || _isConnectionStatusInactive ? '' : classes.hidden; } /** * Hides popover. * * @private * @returns {void} */ _onHidePopover() { // @ts-ignore this.setState({ popoverVisible: false }); } /** * Shows popover. * * @private * @returns {void} */ _onShowPopover() { // @ts-ignore this.setState({ popoverVisible: true }); } /** * Creates a ReactElement for displaying the indicator (GSM bar). * * @returns {ReactElement} */ _renderIndicator() { const { _isConnectionStatusInactive, _isConnectionStatusInterrupted, _connectionIndicatorInactiveDisabled, _videoTrack, classes, iconSize // @ts-ignore } = this.props; 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: IState, ownProps: Props) { const { participantId } = ownProps; const tracks = state['features/base/tracks']; const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state); const participant = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state); let firstVideoTrack; if (sourceNameSignalingEnabled && participant?.isVirtualScreenshareParticipant) { firstVideoTrack = getVirtualScreenshareParticipantTrack(tracks, participantId); } else { firstVideoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId); } const _isConnectionStatusInactive = sourceNameSignalingEnabled ? isTrackStreamingStatusInactive(firstVideoTrack) : isParticipantConnectionStatusInactive(participant); const _isConnectionStatusInterrupted = sourceNameSignalingEnabled ? isTrackStreamingStatusInterrupted(firstVideoTrack) : isParticipantConnectionStatusInterrupted(participant); return { _connectionIndicatorInactiveDisabled: Boolean(state['features/base/config'].connectionIndicators?.inactiveDisabled), _popoverDisabled: state['features/base/config'].connectionIndicators?.disableDetails, _videoTrack: firstVideoTrack, _isConnectionStatusInactive, _isConnectionStatusInterrupted, _sourceName: getSourceNameByParticipantId(state, participantId), _sourceNameSignalingEnabled: sourceNameSignalingEnabled }; } export default translate(connect(_mapStateToProps)( // @ts-ignore withStyles(styles)(ConnectionIndicator)));