import React, { PureComponent } from 'react'; import { FormattedTime, defineMessages, injectIntl } from 'react-intl'; import PropTypes from 'prop-types'; import UserAvatar from '/imports/ui/components/user-avatar/component'; import Icon from '/imports/ui/components/connection-status/icon/component'; import Switch from '/imports/ui/components/switch/component'; import Service from '../service'; import Styled from './styles'; import ConnectionStatusHelper from '../status-helper/container'; const NETWORK_MONITORING_INTERVAL_MS = 2000; const MIN_TIMEOUT = 3000; const intlMessages = defineMessages({ ariaTitle: { id: 'app.connection-status.ariaTitle', description: 'Connection status aria title', }, title: { id: 'app.connection-status.title', description: 'Connection status title', }, description: { id: 'app.connection-status.description', description: 'Connection status description', }, empty: { id: 'app.connection-status.empty', description: 'Connection status empty', }, more: { id: 'app.connection-status.more', description: 'More about conectivity issues', }, audioLabel: { id: 'app.settings.audioTab.label', description: 'Audio label', }, videoLabel: { id: 'app.settings.videoTab.label', description: 'Video label', }, copy: { id: 'app.connection-status.copy', description: 'Copy network data', }, copied: { id: 'app.connection-status.copied', description: 'Copied network data', }, offline: { id: 'app.connection-status.offline', description: 'Offline user', }, dataSaving: { id: 'app.settings.dataSavingTab.description', description: 'Description of data saving', }, webcam: { id: 'app.settings.dataSavingTab.webcam', description: 'Webcam data saving switch', }, screenshare: { id: 'app.settings.dataSavingTab.screenShare', description: 'Screenshare data saving switch', }, on: { id: 'app.switch.onLabel', description: 'label for toggle switch on state', }, off: { id: 'app.switch.offLabel', description: 'label for toggle switch off state', }, no: { id: 'app.connection-status.no', description: 'No to is using turn', }, yes: { id: 'app.connection-status.yes', description: 'Yes to is using turn', }, usingTurn: { id: 'app.connection-status.usingTurn', description: 'User is using turn server', }, jitter: { id: 'app.connection-status.jitter', description: 'Jitter buffer in ms', }, lostPackets: { id: 'app.connection-status.lostPackets', description: 'Number of lost packets', }, audioUploadRate: { id: 'app.connection-status.audioUploadRate', description: 'Label for audio current upload rate', }, audioDownloadRate: { id: 'app.connection-status.audioDownloadRate', description: 'Label for audio current download rate', }, videoUploadRate: { id: 'app.connection-status.videoUploadRate', description: 'Label for video current upload rate', }, videoDownloadRate: { id: 'app.connection-status.videoDownloadRate', description: 'Label for video current download rate', }, connectionStats: { id: 'app.connection-status.connectionStats', description: 'Label for Connection Stats tab', }, myLogs: { id: 'app.connection-status.myLogs', description: 'Label for My Logs tab', }, sessionLogs: { id: 'app.connection-status.sessionLogs', description: 'Label for Session Logs tab', }, next: { id: 'app.connection-status.next', description: 'Label for the next page of the connection stats tab', }, prev: { id: 'app.connection-status.prev', description: 'Label for the previous page of the connection stats tab', }, }); const propTypes = { closeModal: PropTypes.func.isRequired, intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, }; const isConnectionStatusEmpty = (connectionStatus) => { // Check if it's defined if (!connectionStatus) return true; // Check if it's an array if (!Array.isArray(connectionStatus)) return true; // Check if is empty if (connectionStatus.length === 0) return true; return false; }; class ConnectionStatusComponent extends PureComponent { constructor(props) { super(props); const { intl } = this.props; this.help = Service.getHelp(); this.state = { selectedTab: '1', dataPage: '1', dataSaving: props.dataSaving, hasNetworkData: false, networkData: { user: { }, audio: { audioCurrentUploadRate: 0, audioCurrentDownloadRate: 0, jitter: 0, packetsLost: 0, transportStats: {}, }, video: { videoCurrentUploadRate: 0, videoCurrentDownloadRate: 0, }, }, }; this.displaySettingsStatus = this.displaySettingsStatus.bind(this); this.rateInterval = null; this.audioUploadLabel = intl.formatMessage(intlMessages.audioUploadRate); this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate); this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate); this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate); } async componentDidMount() { this.startMonitoringNetwork(); } componentWillUnmount() { Meteor.clearInterval(this.rateInterval); } handleDataSavingChange(key) { const { dataSaving } = this.state; dataSaving[key] = !dataSaving[key]; this.setState(dataSaving); } /** * Start monitoring the network data. * @return {Promise} A Promise that resolves when process started. */ async startMonitoringNetwork() { let previousData = await Service.getNetworkData(); this.rateInterval = Meteor.setInterval(async () => { const data = await Service.getNetworkData(); const { outbound: audioCurrentUploadRate, inbound: audioCurrentDownloadRate, } = Service.calculateBitsPerSecond(data.audio, previousData.audio); const inboundRtp = Service.getDataType(data.audio, 'inbound-rtp')[0]; const jitter = inboundRtp ? inboundRtp.jitterBufferAverage : 0; const packetsLost = inboundRtp ? inboundRtp.packetsLost : 0; const audio = { audioCurrentUploadRate, audioCurrentDownloadRate, jitter, packetsLost, transportStats: data.audio.transportStats, }; const { outbound: videoCurrentUploadRate, inbound: videoCurrentDownloadRate, } = Service.calculateBitsPerSecondFromMultipleData(data.video, previousData.video); const video = { videoCurrentUploadRate, videoCurrentDownloadRate, }; const { user } = data; const networkData = { user, audio, video, }; previousData = data; this.setState({ networkData, hasNetworkData: true, }); }, NETWORK_MONITORING_INTERVAL_MS); } renderEmpty() { const { intl } = this.props; return ( {intl.formatMessage(intlMessages.empty)} ); } displaySettingsStatus(status) { const { intl } = this.props; return ( {status ? intl.formatMessage(intlMessages.on) : intl.formatMessage(intlMessages.off)} ); } /** * Copy network data to clipboard * @param {Object} e Event object from click event * @return {Promise} A Promise that is resolved after data is copied. * * */ async copyNetworkData(e) { const { intl } = this.props; const { networkData, hasNetworkData, } = this.state; if (!hasNetworkData) return; const { target: copyButton } = e; copyButton.innerHTML = intl.formatMessage(intlMessages.copied); const data = JSON.stringify(networkData, null, 2); await navigator.clipboard.writeText(data); this.copyNetworkDataTimeout = setTimeout(() => { copyButton.innerHTML = intl.formatMessage(intlMessages.copy); }, MIN_TIMEOUT); } renderConnections() { const { connectionStatus, intl, } = this.props; const { selectedTab } = this.state; if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty(); let connections = connectionStatus; if (selectedTab === '2') { connections = connections.filter(conn => conn.you); if (isConnectionStatusEmpty(connections)) return this.renderEmpty(); } return connections.map((conn, index) => { const dateTime = new Date(conn.timestamp); return ( {conn.name.toLowerCase().slice(0, 2)} {conn.name} {conn.offline ? ` (${intl.formatMessage(intlMessages.offline)})` : null} ); }); } renderDataSaving() { const { intl, dataSaving, } = this.props; const { viewParticipantsWebcams, viewScreenshare, } = dataSaving; return ( {intl.formatMessage(intlMessages.dataSaving)} {this.displaySettingsStatus(viewParticipantsWebcams)} this.handleDataSavingChange('viewParticipantsWebcams')} ariaLabelledBy="webcam" ariaLabel={intl.formatMessage(intlMessages.webcam)} data-test="dataSavingWebcams" showToggleLabel={false} /> {this.displaySettingsStatus(viewScreenshare)} this.handleDataSavingChange('viewScreenshare')} ariaLabelledBy="screenshare" ariaLabel={intl.formatMessage(intlMessages.screenshare)} data-test="dataSavingScreenshare" showToggleLabel={false} /> ); } /** * Render network data , containing information abount current upload and * download rates * @return {Object} The component to be renderized. */ renderNetworkData() { const { enableNetworkStats } = Meteor.settings.public.app; if (!enableNetworkStats) { return null; } const { audioUploadLabel, audioDownloadLabel, videoUploadLabel, videoDownloadLabel, } = this; const { intl, closeModal } = this.props; const { networkData, dataSaving, dataPage } = this.state; const { audioCurrentUploadRate, audioCurrentDownloadRate, jitter, packetsLost, transportStats, } = networkData.audio; const { videoCurrentUploadRate, videoCurrentDownloadRate, } = networkData.video; let isUsingTurn = '--'; if (transportStats) { switch (transportStats.isUsingTurn) { case true: isUsingTurn = intl.formatMessage(intlMessages.yes); break; case false: isUsingTurn = intl.formatMessage(intlMessages.no); break; default: break; } } function handlePaginationClick(action) { if (action === 'next') { this.setState({ dataPage: '2' }); } else { this.setState({ dataPage: '1' }); } } return ( closeModal(dataSaving, intl)} />
{`${audioUploadLabel}`}
{`${audioCurrentUploadRate}k ↑`}
{`${videoUploadLabel}`}
{`${videoCurrentUploadRate}k ↑`}
{`${intl.formatMessage(intlMessages.jitter)}`}
{`${jitter} ms`}
{`${intl.formatMessage(intlMessages.usingTurn)}`}
{`${isUsingTurn}`}
{`${audioDownloadLabel}`}
{`${audioCurrentDownloadRate}k ↓`}
{`${videoDownloadLabel}`}
{`${videoCurrentDownloadRate}k ↓`}
{`${intl.formatMessage(intlMessages.lostPackets)}`}
{`${packetsLost}`}
Content Hidden
0
); } /** * Renders the clipboard's copy button, for network stats. * @return {Object} - The component to be renderized */ renderCopyDataButton() { const { enableCopyNetworkStatsButton } = Meteor.settings.public.app; if (!enableCopyNetworkStatsButton) { return null; } const { intl } = this.props; const { hasNetworkData } = this.state; return ( {intl.formatMessage(intlMessages.copy)} ); } /** * The navigation bar. * @returns {Object} The component to be renderized. */ renderNavigation() { const { intl } = this.props; const handleTabClick = (event) => { const activeTabElement = document.querySelector('.activeConnectionStatusTab'); const { target } = event; if (activeTabElement) { activeTabElement.classList.remove('activeConnectionStatusTab'); } target.classList.add('activeConnectionStatusTab'); this.setState({ selectedTab: target.dataset.tab, }); } return (
{intl.formatMessage(intlMessages.connectionStats)}
{intl.formatMessage(intlMessages.myLogs)}
{Service.isModerator() && (
{intl.formatMessage(intlMessages.sessionLogs)}
) }
); } render() { const { closeModal, intl, } = this.props; const { dataSaving, selectedTab } = this.state; return ( closeModal(dataSaving, intl)} hideBorder contentLabel={intl.formatMessage(intlMessages.ariaTitle)} data-test="connectionStatusModal" > {intl.formatMessage(intlMessages.title)} {this.renderNavigation()} {selectedTab === '1' ? this.renderNetworkData() : this.renderConnections() } {selectedTab === '1' && this.renderCopyDataButton() } ); } } ConnectionStatusComponent.propTypes = propTypes; export default injectIntl(ConnectionStatusComponent);