diff --git a/react/features/base/participants/components/ParticipantView.native.js b/react/features/base/participants/components/ParticipantView.native.js index b346d895f..300ecf07d 100644 --- a/react/features/base/participants/components/ParticipantView.native.js +++ b/react/features/base/participants/components/ParticipantView.native.js @@ -1,19 +1,25 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { Text, View } from 'react-native'; import { connect } from 'react-redux'; -import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet'; import { prefetch } from '../../../mobile/image-cache'; + +import { translate } from '../../i18n'; +import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet'; import { MEDIA_TYPE, shouldRenderVideoTrack, VideoTrack } from '../../media'; -import { Container } from '../../react'; +import { Container, TintedView } from '../../react'; import { getTrackByMediaTypeAndParticipant } from '../../tracks'; +import { + getAvatarURL, getParticipantById, getParticipantDisplayName +} from '../functions'; + import Avatar from './Avatar'; -import { getAvatarURL, getParticipantById } from '../functions'; import styles from './styles'; /** @@ -54,6 +60,13 @@ class ParticipantView extends Component { */ _connectionStatus: PropTypes.string, + /** + * The name of the participant which this component represents. + * + * @private + */ + _participantName: PropTypes.string, + /** * The video Track of the participant with {@link #participantId}. */ @@ -90,6 +103,17 @@ class ParticipantView extends Component { */ style: PropTypes.object, + /** + * The function to translate human-readable text. + */ + t: PropTypes.func, + + /** + * Indicates if the connectivity info label should be shown, if + * appropriate. It will be shown in case the connection is interrupted. + */ + useConnectivityInfoLabel: PropTypes.bool, + /** * The z-order of the Video of ParticipantView in the stacking space of * all Videos. For more details, refer to the zOrder property of the @@ -98,6 +122,50 @@ class ParticipantView extends Component { zOrder: PropTypes.number }; + /** + * Renders the connection status label, if appropriate. + * + * @param {string} connectionStatus - The status of the participant's + * connection. + * @private + * @returns {ReactElement|null} + */ + _renderConnectionInfo(connectionStatus) { + let messageKey; + + switch (connectionStatus) { + case JitsiParticipantConnectionStatus.INACTIVE: + messageKey = 'connection.LOW_BANDWIDTH'; + break; + case JitsiParticipantConnectionStatus.INTERRUPTED: + messageKey = 'connection.USER_CONNECTION_INTERRUPTED'; + break; + default: + return null; + } + + const { + avatarStyle, + _participantName: displayName, + t + } = this.props; + + // XXX Consider splitting this component into 2: one for the large + // view and one for the thumbnail. Some of these don't apply to both. + const containerStyle = { + ...styles.connectionInfoContainer, + width: avatarStyle.width * 1.5 + }; + + return ( + + + { t(messageKey, { displayName }) } + + + ); + } + /** * Implements React's {@link Component#render()}. * @@ -115,6 +183,10 @@ class ParticipantView extends Component { // FIXME It's currently impossible to have true as the value of // waitForVideoStarted because videoTrack's state videoStarted will be // updated only after videoTrack is rendered. + // XXX Note that, unlike on web, we don't render video when the + // connection status is interrupted, this is because the renderer + // doesn't retain the last frame forever, so we would end up with a + // black screen. const waitForVideoStarted = false; const renderVideo = !this.props._audioOnly @@ -125,6 +197,12 @@ class ParticipantView extends Component { // Is the avatar to be rendered? const renderAvatar = Boolean(!renderVideo && avatar); + // If the connection has problems we will "tint" the video / avatar. + const useTint + = connectionStatus === JitsiParticipantConnectionStatus.INACTIVE + || connectionStatus + === JitsiParticipantConnectionStatus.INTERRUPTED; + return ( } + + { useTint + + // If the connection has problems, tint the video / avatar. + && } + + { this.props.useConnectivityInfoLabel + && this._renderConnectionInfo(connectionStatus) } ); } @@ -196,10 +282,12 @@ function _mapStateToProps(state, ownProps) { participantId); let avatar; let connectionStatus; + let participantName; if (participant) { avatar = getAvatarURL(participant); connectionStatus = participant.connectionStatus; + participantName = getParticipantDisplayName(state); // Avatar (on React Native) now has the ability to generate an // automatically-colored default image when no URI/URL is specified or @@ -223,6 +311,7 @@ function _mapStateToProps(state, ownProps) { _connectionStatus: connectionStatus || JitsiParticipantConnectionStatus.ACTIVE, + _participantName: participantName, _videoTrack: getTrackByMediaTypeAndParticipant( state['features/base/tracks'], @@ -231,4 +320,4 @@ function _mapStateToProps(state, ownProps) { }; } -export default connect(_mapStateToProps)(ParticipantView); +export default translate(connect(_mapStateToProps)(ParticipantView)); diff --git a/react/features/base/participants/components/styles.js b/react/features/base/participants/components/styles.js index f33fea28c..a90731a00 100644 --- a/react/features/base/participants/components/styles.js +++ b/react/features/base/participants/components/styles.js @@ -1,9 +1,31 @@ -import { createStyleSheet } from '../../styles'; +import { BoxModel, ColorPalette, createStyleSheet } from '../../styles'; /** * The styles of the feature base/participants. */ export default createStyleSheet({ + /** + * Style for the text rendered when there is a connectivity problem. + */ + connectionInfoText: { + color: ColorPalette.white, + fontSize: 12, + marginVertical: BoxModel.margin, + marginHorizontal: BoxModel.margin, + textAlign: 'center' + }, + + /** + * Style for the container of the text rendered when there is a + * connectivity problem. + */ + connectionInfoContainer: { + alignSelf: 'center', + backgroundColor: ColorPalette.darkGrey, + borderRadius: 20, + marginTop: BoxModel.margin + }, + /** * {@code ParticipantView} style. */ diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index 7fd539104..3cf8ae194 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -122,6 +122,37 @@ export function getParticipantCount(stateful: Object | Function) { return getParticipants(stateful).length; } +/** + * Returns participant's display name. + * FIXME: remove the hardcoded strings once interfaceConfig is stored in redux + * and merge with a similarly named method in conference.js. + * + * @param {(Function|Object)} stateful - The (whole) redux state, or redux's + * {@code getState} function to be used to retrieve the state. + * @param {string} id - The ID of the participant's display name to retrieve. + * @private + * @returns {string} + */ +export function getParticipantDisplayName( + stateful: Object | Function, id: string) { + const participant = getParticipantById(stateful, id); + + if (participant) { + if (participant.name) { + return participant.name; + } + + if (participant.local) { + return typeof interfaceConfig === 'object' + ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME + : 'me'; + } + } + + return typeof interfaceConfig === 'object' + ? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME + : 'Fellow Jitster'; +} /** * Selectors for getting all known participants with fake participants filtered diff --git a/react/features/large-video/components/LargeVideo.native.js b/react/features/large-video/components/LargeVideo.native.js index e20855db8..ccf474557 100644 --- a/react/features/large-video/components/LargeVideo.native.js +++ b/react/features/large-video/components/LargeVideo.native.js @@ -41,6 +41,7 @@ class LargeVideo extends Component<*> { avatarStyle = { styles.avatar } participantId = { this.props._participantId } style = { styles.largeVideo } + useConnectivityInfoLabel = { true } zOrder = { 0 } /> ); }