fix(rn,connection-indicator) align rendering with web

Use the same way for calculating que perceived quality and display it.
This commit is contained in:
Saúl Ibarra Corretgé 2022-11-11 12:11:30 +01:00 committed by Saúl Ibarra Corretgé
parent 48efe36cdf
commit 14fcd153e5
15 changed files with 393 additions and 174 deletions

View File

@ -1,3 +1,8 @@
/**
* IMPORTANT: this file is deprecated. All of these colors should be moved to
* the theme instead.
*/
/**
* The application's definition of the default color black.
*/
@ -18,29 +23,12 @@ export const ColorPalette = {
* the sake of consistency.
*/
black: BLACK,
blackBlue: 'rgb(0, 3, 6)',
blue: '#17A0DB',
blueHighlight: '#1081b2',
buttonUnderlay: '#495258',
darkGrey: '#555555',
darkBackground: 'rgb(19,21,25)',
green: '#40b183',
lightGrey: '#AAAAAA',
overflowMenuItemUnderlay: '#EEEEEE',
red: '#D00000',
transparent: 'rgba(0, 0, 0, 0)',
toggled: 'rgba(255,255,255,.15)',
warning: 'rgb(215, 121, 118)',
white: '#FFFFFF',
/**
* These are colors from the atlaskit to be used on mobile, when needed.
*
* FIXME: Maybe a better solution would be good, or a native packaging of
* the respective atlaskit components.
*/
G400: '#00875A', // Slime
N500: '#42526E', // McFanning
R400: '#DE350B', // Red dirt
Y200: '#FFC400' // Pub mix
white: '#FFFFFF'
};

View File

@ -2,7 +2,7 @@
import React, { Component, Fragment } from 'react';
import { statsEmitter } from '../../../connection-indicator';
import statsEmitter from '../../../connection-indicator/statsEmitter';
import { getLocalParticipant } from '../../participants';
import { connect } from '../../redux';
import { isTestModeEnabled } from '../functions';

View File

@ -1,4 +1,5 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { StyleProp, Text, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
@ -57,7 +58,9 @@ const TitleBar = (props: IProps): JSX.Element => {
<VideoQualityLabel />
</View>
<ConnectionIndicator
// @ts-ignore
iconStyle = { styles.connectionIndicatorIcon }
// @ts-ignore
participantId = { localParticipantId } />
<View style = { styles.headerLabels as StyleProp<ViewStyle> }>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />

View File

@ -209,7 +209,7 @@ class AbstractConnectionIndicator<P: Props, S: State> extends Component<P, S> {
*/
export function mapStateToProps(state: Object) {
return {
_autoHideTimeout: state['features/base/config'].connectionIndicators.autoHideTimeout ?? defaultAutoHideTimeout
_autoHideTimeout: state['features/base/config'].connectionIndicators?.autoHideTimeout ?? defaultAutoHideTimeout
};
}

View File

@ -1,3 +0,0 @@
// @flow
export * from './native';

View File

@ -1,3 +0,0 @@
// @flow
export * from './web';

View File

@ -1,68 +0,0 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import { IconConnection } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import indicatorStyles from '../../../filmstrip/components/native/styles';
import AbstractConnectionIndicator, {
type Props,
type State
} from '../AbstractConnectionIndicator';
import { CONNECTOR_INDICATOR_COLORS, iconStyle } from './styles';
/**
* Implements an indicator to show the quality of the connection of a participant.
*/
class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
/**
* Initializes a new {@code ConnectionIndicator} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
autoHideTimeout: undefined,
showIndicator: false,
stats: {}
};
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { showIndicator, stats } = this.state;
const { percent } = stats;
if (!showIndicator || typeof percent === 'undefined') {
return null;
}
// Signal level on a scale 0..2
const signalLevel = Math.floor(percent / 33.4);
return (
<View
style = {{
...indicatorStyles.indicatorContainer,
backgroundColor: CONNECTOR_INDICATOR_COLORS[signalLevel]
}}>
<BaseIndicator
icon = { IconConnection }
iconStyle = { this.props.iconStyle || iconStyle } />
</View>
);
}
}
export default connect()(ConnectionIndicator);

View File

@ -0,0 +1,216 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { View } from 'react-native';
import { IReduxState } from '../../../app/types';
import { IconConnection } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import {
getLocalParticipant,
getParticipantById,
isScreenShareParticipant
} from '../../../base/participants/functions';
// @ts-ignore
import BaseIndicator from '../../../base/react/components/native/BaseIndicator';
import { connect } from '../../../base/redux/functions';
import {
getTrackByMediaTypeAndParticipant
} from '../../../base/tracks/functions.native';
// @ts-ignore
import indicatorStyles from '../../../filmstrip/components/native/styles';
import {
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../functions';
import AbstractConnectionIndicator, {
type Props as AbstractProps,
mapStateToProps as _abstractMapStateToProps
// @ts-ignore
} from '../AbstractConnectionIndicator';
import {
CONNECTOR_INDICATOR_COLORS,
CONNECTOR_INDICATOR_LOST,
CONNECTOR_INDICATOR_OTHER,
iconStyle
} from './styles';
type IProps = AbstractProps & {
/**
* Whether connection indicators are disabled or not.
*/
_connectionIndicatorDisabled: boolean;
/**
* Whether the inactive connection indicator is disabled or not.
*/
_connectionIndicatorInactiveDisabled: boolean;
/**
* Whether the connection is inactive or not.
*/
_isConnectionStatusInactive: boolean;
/**
* Whether the connection is interrupted or not.
*/
_isConnectionStatusInterrupted: boolean;
/**
* Whether the current participant is a virtual screenshare.
*/
_isVirtualScreenshareParticipant: boolean;
/**
* Redux dispatch function.
*/
dispatch: Function;
/**
* Icon style override.
*/
iconStyle: any;
};
type IState = {
autoHideTimeout: number | undefined;
showIndicator: boolean;
stats: any;
};
/**
* Implements an indicator to show the quality of the connection of a participant.
*/
class ConnectionIndicator extends AbstractConnectionIndicator<IProps, IState> {
/**
* Initializes a new {@code ConnectionIndicator} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
// @ts-ignore
this.state = {
autoHideTimeout: undefined,
showIndicator: false,
stats: {}
};
}
/**
* Get the icon configuration from CONNECTOR_INDICATOR_COLORS which has a percentage
* that matches or exceeds the passed in percentage. The implementation
* assumes CONNECTOR_INDICATOR_COLORS 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 CONNECTOR_INDICATOR_COLORS.find(x => percent >= x.percent) || {};
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
_connectionIndicatorInactiveDisabled,
_connectionIndicatorDisabled,
_isVirtualScreenshareParticipant,
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
// @ts-ignore
} = this.props;
const {
showIndicator,
stats
// @ts-ignore
} = this.state;
const { percent } = stats;
if (!showIndicator || typeof percent === 'undefined'
|| _connectionIndicatorDisabled || _isVirtualScreenshareParticipant) {
return null;
}
let indicatorColor;
if (_isConnectionStatusInactive) {
if (_connectionIndicatorInactiveDisabled) {
return null;
}
indicatorColor = CONNECTOR_INDICATOR_OTHER;
} else if (_isConnectionStatusInterrupted) {
indicatorColor = CONNECTOR_INDICATOR_LOST;
} else {
const displayConfig = this._getDisplayConfiguration(percent);
if (!displayConfig) {
return null;
}
indicatorColor = displayConfig.color;
}
return (
<View
style = {{
...indicatorStyles.indicatorContainer,
backgroundColor: indicatorColor
}}>
<BaseIndicator
icon = { IconConnection }
// @ts-ignore
iconStyle = { this.props.iconStyle || iconStyle } />
</View>
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {IProps} ownProps - The own props of the component.
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const { participantId } = ownProps;
const tracks = state['features/base/tracks'];
const participant = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
const _isVirtualScreenshareParticipant = isScreenShareParticipant(participant);
let _isConnectionStatusInactive;
let _isConnectionStatusInterrupted;
if (!_isVirtualScreenshareParticipant) {
const _videoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
_isConnectionStatusInactive = isTrackStreamingStatusInactive(_videoTrack);
_isConnectionStatusInterrupted = isTrackStreamingStatusInterrupted(_videoTrack);
}
return {
..._abstractMapStateToProps(state),
_connectionIndicatorInactiveDisabled:
Boolean(state['features/base/config'].connectionIndicators?.inactiveDisabled),
_connectionIndicatorDisabled:
Boolean(state['features/base/config'].connectionIndicators?.disabled),
_isVirtualScreenshareParticipant,
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
};
}
// @ts-ignore
export default connect(_mapStateToProps)(ConnectionIndicator);

View File

@ -1,3 +0,0 @@
// @flow
export { default as ConnectionIndicator } from './ConnectionIndicator';

View File

@ -1,13 +0,0 @@
// @flow
import { ColorPalette } from '../../../base/styles';
export const CONNECTOR_INDICATOR_COLORS = [
ColorPalette.red,
ColorPalette.Y200,
ColorPalette.green
];
export const iconStyle = {
fontSize: 14
};

View File

@ -0,0 +1,32 @@
/* eslint-disable lines-around-comment */
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
// @ts-ignore
import { INDICATOR_DISPLAY_THRESHOLD } from '../AbstractConnectionIndicator';
export const CONNECTOR_INDICATOR_LOST = BaseTheme.palette.ui05;
export const CONNECTOR_INDICATOR_OTHER = BaseTheme.palette.action01;
export const CONNECTOR_INDICATOR_COLORS = [
// Full (3 bars)
{
color: BaseTheme.palette.success01,
percent: INDICATOR_DISPLAY_THRESHOLD
},
// 2 bars.
{
color: BaseTheme.palette.warning01,
percent: 10
},
// 1 bar.
{
color: BaseTheme.palette.iconError,
percent: 0
}
];
export const iconStyle = {
fontSize: 14
};

View File

@ -27,7 +27,8 @@ import {
import AbstractConnectionIndicator, {
type Props as AbstractProps,
type State as AbstractState,
INDICATOR_DISPLAY_THRESHOLD
INDICATOR_DISPLAY_THRESHOLD,
mapStateToProps as _abstractMapStateToProps
// @ts-ignore
} from '../AbstractConnectionIndicator';
@ -386,6 +387,7 @@ export function _mapStateToProps(state: IReduxState, ownProps: Props) {
const _isConnectionStatusInterrupted = isTrackStreamingStatusInterrupted(_videoTrack);
return {
..._abstractMapStateToProps(state),
_connectionIndicatorInactiveDisabled:
Boolean(state['features/base/config'].connectionIndicators?.inactiveDisabled),
_isVirtualScreenshareParticipant: isScreenShareParticipant(participant),

View File

@ -1,5 +0,0 @@
// @flow
export * from './components';
export { default as statsEmitter } from './statsEmitter';

View File

@ -25,7 +25,7 @@ import {
getVideoTrackByParticipant,
trackStreamingStatusChanged
} from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import ConnectionIndicator from '../../../connection-indicator/components/native/ConnectionIndicator';
import { DisplayNameLabel } from '../../../display-name';
import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
import {
@ -191,6 +191,8 @@ class Thumbnail extends PureComponent<Props> {
dispatch(showSharedVideoMenu(_participantId));
}
// TODO: add support for getting info about the virtual screen shares.
if (!_fakeParticipant) {
dispatch(showContextMenuDetails(_participantId, _local));
}

View File

@ -1,17 +1,34 @@
import React, { Component } from 'react';
/* eslint-disable lines-around-comment */
import React, { PureComponent } from 'react';
import { Text, View } from 'react-native';
import { withTheme } from 'react-native-paper';
import { Avatar } from '../../../base/avatar';
import { BottomSheet, hideSheet } from '../../../base/dialog';
import { IReduxState } from '../../../app/types';
// @ts-ignore
import Avatar from '../../../base/avatar/components/Avatar';
import { hideSheet } from '../../../base/dialog/actions';
// @ts-ignore
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
// @ts-ignore
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import { translate } from '../../../base/i18n';
import { IconArrowDownLarge, IconArrowUpLarge } from '../../../base/icons';
import { getParticipantDisplayName } from '../../../base/participants';
import { BaseIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import { translate } from '../../../base/i18n/functions';
import { IconArrowDownLarge, IconArrowUpLarge } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { getParticipantDisplayName } from '../../../base/participants/functions';
// @ts-ignore
import BaseIndicator from '../../../base/react/components/native/BaseIndicator';
import { connect } from '../../../base/redux/functions';
import {
getTrackByMediaTypeAndParticipant
} from '../../../base/tracks/functions.native';
import {
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../../connection-indicator/functions';
import statsEmitter from '../../../connection-indicator/statsEmitter';
// @ts-ignore
import styles from './styles';
/**
@ -20,62 +37,87 @@ import styles from './styles';
const AVATAR_SIZE = 25;
const CONNECTION_QUALITY = [
'Low',
'Medium',
'Good'
// Full (3 bars)
{
msg: 'connectionindicator.quality.good',
percent: 30 // INDICATOR_DISPLAY_THRESHOLD
},
// 2 bars.
{
msg: 'connectionindicator.quality.nonoptimal',
percent: 10
},
// 1 bar.
{
msg: 'connectionindicator.quality.poor',
percent: 0
}
];
export type Props = {
type IProps = {
/**
* The Redux dispatch function.
* Whether this participant's connection is inactive.
*/
dispatch: Function,
_isConnectionStatusInactive: boolean;
/**
* The ID of the participant that this button is supposed to pin.
* Whether this participant's connection is interrupted.
*/
participantID: string,
_isConnectionStatusInterrupted: boolean;
/**
* True if the menu is currently open, false otherwise.
*/
_isOpen: boolean,
_isOpen: boolean;
/**
* Display name of the participant retrieved from Redux.
*/
_participantDisplayName: string,
_participantDisplayName: string;
/**
* The Redux dispatch function.
*/
dispatch: Function;
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string;
/**
* The function to be used to translate i18n labels.
*/
t: Function,
t: Function;
/**
* Theme used for styles.
*/
theme: Object
}
theme: any;
};
/**
* The type of the React {@code Component} state of {@link ConnectionStatusComponent}.
*/
type State = {
resolutionString: string,
downloadString: string,
uploadString: string,
packetLostDownloadString: string,
packetLostUploadString: string,
serverRegionString: string,
codecString: string,
connectionString: string
type IState = {
codecString: string;
connectionString: string;
downloadString: string;
packetLostDownloadString: string;
packetLostUploadString: string;
resolutionString: string;
serverRegionString: string;
uploadString: string;
};
/**
* Class to implement a popup menu that show the connection statistics.
*/
class ConnectionStatusComponent extends Component<Props, State> {
class ConnectionStatusComponent extends PureComponent<IProps, IState> {
/**
* Constructor of the component.
@ -85,7 +127,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this._onStatsUpdated = this._onStatsUpdated.bind(this);
@ -121,15 +163,15 @@ class ConnectionStatusComponent extends Component<Props, State> {
<View style = { styles.statsWrapper }>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ `${t('connectionindicator.status')} ` }
{ t('connectionindicator.status') }
</Text>
<Text style = { styles.statsInfoText }>
{ this.state.connectionString }
{ t(this.state.connectionString) }
</Text>
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ `${t('connectionindicator.bitrate')}` }
{ t('connectionindicator.bitrate') }
</Text>
<BaseIndicator
icon = { IconArrowDownLarge }
@ -150,7 +192,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ `${t('connectionindicator.packetloss')}` }
{ t('connectionindicator.packetloss') }
</Text>
<BaseIndicator
icon = { IconArrowDownLarge }
@ -171,7 +213,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ `${t('connectionindicator.resolution')} ` }
{ t('connectionindicator.resolution') }
</Text>
<Text style = { styles.statsInfoText }>
{ this.state.resolutionString }
@ -179,7 +221,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
</View>
<View style = { styles.statsInfoCell }>
<Text style = { styles.statsTitleText }>
{ `${t('connectionindicator.codecs')}` }
{ t('connectionindicator.codecs') }
</Text>
<Text style = { styles.statsInfoText }>
{ this.state.codecString }
@ -206,7 +248,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @inheritdoc
* returns {void}
*/
componentDidUpdate(prevProps: Props) {
componentDidUpdate(prevProps: IProps) {
if (prevProps.participantID !== this.props.participantID) {
statsEmitter.unsubscribeToClientStats(
prevProps.participantID, this._onStatsUpdated);
@ -215,8 +257,6 @@ class ConnectionStatusComponent extends Component<Props, State> {
}
}
_onStatsUpdated: Object => void;
/**
* Callback invoked when new connection stats associated with the passed in
* user ID are available. Will update the component's display of current
@ -239,9 +279,8 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {State}
*/
_buildState(stats) {
_buildState(stats: any) {
const { download: downloadBitrate, upload: uploadBitrate } = this._extractBitrate(stats) ?? {};
const { download: downloadPacketLost, upload: uploadPacketLost } = this._extractPacketLost(stats) ?? {};
return {
@ -254,7 +293,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
? this.state.packetLostUploadString : `${uploadPacketLost}%`,
serverRegionString: this._extractServer(stats) ?? this.state.serverRegionString,
codecString: this._extractCodecs(stats) ?? this.state.codecString,
connectionString: this._extractConnection(stats) ?? this.state.connectionString
connectionString: this._extractConnection(stats)
};
}
@ -265,7 +304,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {string}
*/
_extractResolutionString(stats) {
_extractResolutionString(stats: any) {
const { framerate, resolution } = stats;
const resolutionString = Object.keys(resolution || {})
@ -290,7 +329,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {{ download, upload }}
*/
_extractBitrate(stats) {
_extractBitrate(stats: any) {
return stats.bitrate;
}
@ -301,7 +340,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {{ download, upload }}
*/
_extractPacketLost(stats) {
_extractPacketLost(stats: any) {
return stats.packetLoss;
}
@ -312,7 +351,7 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {string}
*/
_extractServer(stats) {
_extractServer(stats: any) {
return stats.serverRegion;
}
@ -323,17 +362,17 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {string}
*/
_extractCodecs(stats) {
_extractCodecs(stats: any) {
const { codec } = stats;
let codecString;
if (codec) {
const audioCodecs = Object.values(codec)
.map(c => c.audio)
.map((c: any) => c.audio)
.filter(Boolean);
const videoCodecs = Object.values(codec)
.map(c => c.video)
.map((c: any) => c.video)
.filter(Boolean);
if (audioCodecs.length || videoCodecs.length) {
@ -352,14 +391,39 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {string}
*/
_extractConnection(stats) {
_extractConnection(stats: any) {
const { connectionQuality } = stats;
const {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
} = this.props;
if (connectionQuality) {
const signalLevel = Math.floor(connectionQuality / 33.4);
return CONNECTION_QUALITY[signalLevel];
if (_isConnectionStatusInactive) {
return 'connectionindicator.quality.inactive';
} else if (_isConnectionStatusInterrupted) {
return 'connectionindicator.quality.lost';
} else if (typeof connectionQuality === 'undefined') {
return 'connectionindicator.quality.good';
}
const qualityConfig = this._getQualityConfig(connectionQuality);
return qualityConfig.msg;
}
/**
* Get the quality configuration from CONNECTION_QUALITY which has a percentage
* that matches or exceeds the passed in percentage. The implementation
* assumes CONNECTION_QUALITY 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}
*/
_getQualityConfig(percent: number): any {
return CONNECTION_QUALITY.find(x => percent >= x.percent) || {};
}
/**
@ -406,12 +470,19 @@ class ConnectionStatusComponent extends Component<Props, State> {
* @private
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const { participantID } = ownProps;
const tracks = state['features/base/tracks'];
const _videoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const _isConnectionStatusInactive = isTrackStreamingStatusInactive(_videoTrack);
const _isConnectionStatusInterrupted = isTrackStreamingStatusInterrupted(_videoTrack);
return {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_participantDisplayName: getParticipantDisplayName(state, participantID)
};
}
// @ts-ignore
export default translate(connect(_mapStateToProps)(withTheme(ConnectionStatusComponent)));