Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

ConnectionStatus.tsx 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import React, { useCallback, useState } from 'react';
  2. import { WithTranslation } from 'react-i18next';
  3. import { connect } from 'react-redux';
  4. import { makeStyles } from 'tss-react/mui';
  5. import { translate } from '../../../i18n/functions';
  6. import Icon from '../../../icons/components/Icon';
  7. import { IconArrowDown, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
  8. import { withPixelLineHeight } from '../../../styles/functions.web';
  9. import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
  10. import { CONNECTION_TYPE } from '../../constants';
  11. import { getConnectionData } from '../../functions';
  12. interface IProps extends WithTranslation {
  13. /**
  14. * List of strings with details about the connection.
  15. */
  16. connectionDetails?: string[];
  17. /**
  18. * The type of the connection. Can be: 'none', 'poor', 'nonOptimal' or 'good'.
  19. */
  20. connectionType?: string;
  21. }
  22. const useStyles = makeStyles()(theme => {
  23. return {
  24. connectionStatus: {
  25. color: '#fff',
  26. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  27. position: 'absolute',
  28. width: '100%',
  29. [theme.breakpoints.down(400)]: {
  30. margin: 0,
  31. width: '100%'
  32. },
  33. '@media (max-width: 720px)': {
  34. margin: `${theme.spacing(4)} auto`,
  35. position: 'fixed',
  36. top: 0,
  37. width: PREJOIN_DEFAULT_CONTENT_WIDTH
  38. },
  39. // mobile phone landscape
  40. '@media (max-height: 420px)': {
  41. display: 'none'
  42. },
  43. '& .con-status-header': {
  44. backgroundColor: 'rgba(0, 0, 0, 0.7)',
  45. alignItems: 'center',
  46. display: 'flex',
  47. padding: '12px 16px',
  48. borderRadius: theme.shape.borderRadius
  49. },
  50. '& .con-status-circle': {
  51. borderRadius: '50%',
  52. display: 'inline-block',
  53. padding: theme.spacing(1),
  54. marginRight: theme.spacing(2)
  55. },
  56. '& .con-status--good': {
  57. background: '#31B76A'
  58. },
  59. '& .con-status--poor': {
  60. background: '#E12D2D'
  61. },
  62. '& .con-status--non-optimal': {
  63. background: '#E39623'
  64. },
  65. '& .con-status-arrow': {
  66. marginLeft: 'auto',
  67. transition: 'background-color 0.16s ease-out'
  68. },
  69. '& .con-status-arrow--up': {
  70. transform: 'rotate(180deg)'
  71. },
  72. '& .con-status-arrow > svg': {
  73. cursor: 'pointer'
  74. },
  75. '& .con-status-arrow:hover': {
  76. backgroundColor: 'rgba(1, 1, 1, 0.1)'
  77. },
  78. '& .con-status-text': {
  79. textAlign: 'center'
  80. },
  81. '& .con-status-details': {
  82. backgroundColor: 'rgba(0, 0, 0, 0.7)',
  83. borderTop: '1px solid #5E6D7A',
  84. padding: theme.spacing(3),
  85. transition: 'opacity 0.16s ease-out'
  86. },
  87. '& .con-status-details-visible': {
  88. opacity: 1
  89. },
  90. '& .con-status-details-hidden': {
  91. opacity: 0
  92. }
  93. }
  94. };
  95. });
  96. const CONNECTION_TYPE_MAP: {
  97. [key: string]: {
  98. connectionClass: string;
  99. connectionText: string;
  100. icon: Function;
  101. };
  102. } = {
  103. [CONNECTION_TYPE.POOR]: {
  104. connectionClass: 'con-status--poor',
  105. icon: IconWifi1Bar,
  106. connectionText: 'prejoin.connection.poor'
  107. },
  108. [CONNECTION_TYPE.NON_OPTIMAL]: {
  109. connectionClass: 'con-status--non-optimal',
  110. icon: IconWifi2Bars,
  111. connectionText: 'prejoin.connection.nonOptimal'
  112. },
  113. [CONNECTION_TYPE.GOOD]: {
  114. connectionClass: 'con-status--good',
  115. icon: IconWifi3Bars,
  116. connectionText: 'prejoin.connection.good'
  117. }
  118. };
  119. /**
  120. * Component displaying information related to the connection & audio/video quality.
  121. *
  122. * @param {IProps} props - The props of the component.
  123. * @returns {ReactElement}
  124. */
  125. function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
  126. const { classes } = useStyles();
  127. const [ showDetails, toggleDetails ] = useState(false);
  128. const arrowClassName = showDetails
  129. ? 'con-status-arrow con-status-arrow--up'
  130. : 'con-status-arrow';
  131. const detailsText = connectionDetails?.map(d => t(d)).join(' ');
  132. const detailsClassName = showDetails
  133. ? 'con-status-details-visible'
  134. : 'con-status-details-hidden';
  135. const onToggleDetails = useCallback(e => {
  136. e.preventDefault();
  137. toggleDetails(!showDetails);
  138. }, [ showDetails, toggleDetails ]);
  139. const onKeyPressToggleDetails = useCallback(e => {
  140. if (toggleDetails && (e.key === ' ' || e.key === 'Enter')) {
  141. e.preventDefault();
  142. toggleDetails(!showDetails);
  143. }
  144. }, [ showDetails, toggleDetails ]);
  145. if (connectionType === CONNECTION_TYPE.NONE) {
  146. return null;
  147. }
  148. const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType ?? ''];
  149. return (
  150. <div className = { classes.connectionStatus }>
  151. <div
  152. aria-level = { 1 }
  153. className = 'con-status-header'
  154. role = 'heading'>
  155. <div className = { `con-status-circle ${connectionClass}` }>
  156. <Icon
  157. size = { 16 }
  158. src = { icon } />
  159. </div>
  160. <span
  161. aria-hidden = { !showDetails }
  162. className = 'con-status-text'
  163. id = 'connection-status-description'>{t(connectionText)}</span>
  164. <Icon
  165. ariaDescribedBy = 'connection-status-description'
  166. ariaPressed = { showDetails }
  167. className = { arrowClassName }
  168. onClick = { onToggleDetails }
  169. onKeyPress = { onKeyPressToggleDetails }
  170. role = 'button'
  171. size = { 24 }
  172. src = { IconArrowDown }
  173. tabIndex = { 0 } />
  174. </div>
  175. <div
  176. aria-level = { 2 }
  177. className = { `con-status-details ${detailsClassName}` }
  178. role = 'heading'>
  179. {detailsText}</div>
  180. </div>
  181. );
  182. }
  183. /**
  184. * Maps (parts of) the redux state to the React {@code Component} props.
  185. *
  186. * @param {Object} state - The redux state.
  187. * @returns {Object}
  188. */
  189. function mapStateToProps() {
  190. const { connectionDetails, connectionType } = getConnectionData();
  191. return {
  192. connectionDetails,
  193. connectionType
  194. };
  195. }
  196. export default translate(connect(mapStateToProps)(ConnectionStatus));