Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. // @flow
  2. import InlineDialog from '@atlaskit/inline-dialog';
  3. import React, { Component } from 'react';
  4. import { Avatar } from '../../../base/avatar';
  5. import { isNameReadOnly } from '../../../base/config';
  6. import { translate } from '../../../base/i18n';
  7. import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons';
  8. import { isVideoMutedByUser } from '../../../base/media';
  9. import { getLocalParticipant } from '../../../base/participants';
  10. import { ActionButton, InputField, PreMeetingScreen } from '../../../base/premeeting';
  11. import { connect } from '../../../base/redux';
  12. import { getDisplayName, updateSettings } from '../../../base/settings';
  13. import { getLocalJitsiVideoTrack } from '../../../base/tracks';
  14. import {
  15. joinConference as joinConferenceAction,
  16. joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
  17. setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
  18. } from '../../actions';
  19. import {
  20. isDeviceStatusVisible,
  21. isDisplayNameRequired,
  22. isJoinByPhoneButtonVisible,
  23. isJoinByPhoneDialogVisible,
  24. isPrejoinDisplayNameVisible
  25. } from '../../functions';
  26. import DropdownButton from './DropdownButton';
  27. import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
  28. type Props = {
  29. /**
  30. * Indicates whether the display name is editable.
  31. */
  32. canEditDisplayName: boolean,
  33. /**
  34. * Flag signaling if the device status is visible or not.
  35. */
  36. deviceStatusVisible: boolean,
  37. /**
  38. * If join by phone button should be visible.
  39. */
  40. hasJoinByPhoneButton: boolean,
  41. /**
  42. * Joins the current meeting.
  43. */
  44. joinConference: Function,
  45. /**
  46. * Joins the current meeting without audio.
  47. */
  48. joinConferenceWithoutAudio: Function,
  49. /**
  50. * The name of the user that is about to join.
  51. */
  52. name: string,
  53. /**
  54. * Updates settings.
  55. */
  56. updateSettings: Function,
  57. /**
  58. * Local participant id.
  59. */
  60. participantId: string,
  61. /**
  62. * The prejoin config.
  63. */
  64. prejoinConfig?: Object,
  65. /**
  66. * Whether the name input should be read only or not.
  67. */
  68. readOnlyName: boolean,
  69. /**
  70. * Sets visibility of the 'JoinByPhoneDialog'.
  71. */
  72. setJoinByPhoneDialogVisiblity: Function,
  73. /**
  74. * Flag signaling the visibility of camera preview.
  75. */
  76. showCameraPreview: boolean,
  77. /**
  78. * If should show an error when joining without a name.
  79. */
  80. showErrorOnJoin: boolean,
  81. /**
  82. * If 'JoinByPhoneDialog' is visible or not.
  83. */
  84. showDialog: boolean,
  85. /**
  86. * Used for translation.
  87. */
  88. t: Function,
  89. /**
  90. * The JitsiLocalTrack to display.
  91. */
  92. videoTrack: ?Object
  93. };
  94. type State = {
  95. /**
  96. * Flag controlling the visibility of the error label.
  97. */
  98. showError: boolean,
  99. /**
  100. * Flag controlling the visibility of the 'join by phone' buttons.
  101. */
  102. showJoinByPhoneButtons: boolean
  103. }
  104. /**
  105. * This component is displayed before joining a meeting.
  106. */
  107. class Prejoin extends Component<Props, State> {
  108. /**
  109. * Initializes a new {@code Prejoin} instance.
  110. *
  111. * @inheritdoc
  112. */
  113. constructor(props) {
  114. super(props);
  115. this.state = {
  116. showError: false,
  117. showJoinByPhoneButtons: false
  118. };
  119. this._closeDialog = this._closeDialog.bind(this);
  120. this._showDialog = this._showDialog.bind(this);
  121. this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
  122. this._onDropdownClose = this._onDropdownClose.bind(this);
  123. this._onOptionsClick = this._onOptionsClick.bind(this);
  124. this._setName = this._setName.bind(this);
  125. this._onJoinConferenceWithoutAudioKeyPress = this._onJoinConferenceWithoutAudioKeyPress.bind(this);
  126. this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
  127. this._onJoinKeyPress = this._onJoinKeyPress.bind(this);
  128. this._getExtraJoinButtons = this._getExtraJoinButtons.bind(this);
  129. this.showDisplayNameField = props.canEditDisplayName || props.showErrorOnJoin;
  130. }
  131. _onJoinButtonClick: () => void;
  132. /**
  133. * Handler for the join button.
  134. *
  135. * @param {Object} e - The synthetic event.
  136. * @returns {void}
  137. */
  138. _onJoinButtonClick() {
  139. if (this.props.showErrorOnJoin) {
  140. this.setState({
  141. showError: true
  142. });
  143. return;
  144. }
  145. this.setState({ showError: false });
  146. this.props.joinConference();
  147. }
  148. _onJoinKeyPress: (Object) => void;
  149. /**
  150. * KeyPress handler for accessibility.
  151. *
  152. * @param {Object} e - The key event to handle.
  153. *
  154. * @returns {void}
  155. */
  156. _onJoinKeyPress(e) {
  157. if (e.key === ' ' || e.key === 'Enter') {
  158. e.preventDefault();
  159. this._onJoinButtonClick();
  160. }
  161. }
  162. _onDropdownClose: () => void;
  163. /**
  164. * Closes the dropdown.
  165. *
  166. * @returns {void}
  167. */
  168. _onDropdownClose() {
  169. this.setState({
  170. showJoinByPhoneButtons: false
  171. });
  172. }
  173. _onOptionsClick: () => void;
  174. /**
  175. * Displays the join by phone buttons dropdown.
  176. *
  177. * @param {Object} e - The synthetic event.
  178. * @returns {void}
  179. */
  180. _onOptionsClick(e) {
  181. e.stopPropagation();
  182. this.setState({
  183. showJoinByPhoneButtons: !this.state.showJoinByPhoneButtons
  184. });
  185. }
  186. _setName: () => void;
  187. /**
  188. * Sets the guest participant name.
  189. *
  190. * @param {string} displayName - Participant name.
  191. * @returns {void}
  192. */
  193. _setName(displayName) {
  194. this.props.updateSettings({
  195. displayName
  196. });
  197. }
  198. _closeDialog: () => void;
  199. /**
  200. * Closes the join by phone dialog.
  201. *
  202. * @returns {undefined}
  203. */
  204. _closeDialog() {
  205. this.props.setJoinByPhoneDialogVisiblity(false);
  206. }
  207. _showDialog: () => void;
  208. /**
  209. * Displays the dialog for joining a meeting by phone.
  210. *
  211. * @returns {undefined}
  212. */
  213. _showDialog() {
  214. this.props.setJoinByPhoneDialogVisiblity(true);
  215. this._onDropdownClose();
  216. }
  217. _showDialogKeyPress: (Object) => void;
  218. /**
  219. * KeyPress handler for accessibility.
  220. *
  221. * @param {Object} e - The key event to handle.
  222. *
  223. * @returns {void}
  224. */
  225. _showDialogKeyPress(e) {
  226. if (e.key === ' ' || e.key === 'Enter') {
  227. e.preventDefault();
  228. this._showDialog();
  229. }
  230. }
  231. _onJoinConferenceWithoutAudioKeyPress: (Object) => void;
  232. /**
  233. * KeyPress handler for accessibility.
  234. *
  235. * @param {Object} e - The key event to handle.
  236. *
  237. * @returns {void}
  238. */
  239. _onJoinConferenceWithoutAudioKeyPress(e) {
  240. if (this.props.joinConferenceWithoutAudio
  241. && (e.key === ' '
  242. || e.key === 'Enter')) {
  243. e.preventDefault();
  244. this.props.joinConferenceWithoutAudio();
  245. }
  246. }
  247. _getExtraJoinButtons: () => Object;
  248. /**
  249. * Gets the list of extra join buttons.
  250. *
  251. * @returns {Object} - The list of extra buttons.
  252. */
  253. _getExtraJoinButtons() {
  254. const { joinConferenceWithoutAudio, t } = this.props;
  255. const noAudio = {
  256. key: 'no-audio',
  257. dataTestId: 'prejoin.joinWithoutAudio',
  258. icon: IconVolumeOff,
  259. label: t('prejoin.joinWithoutAudio'),
  260. onButtonClick: joinConferenceWithoutAudio,
  261. onKeyPressed: this._onJoinConferenceWithoutAudioKeyPress
  262. };
  263. const byPhone = {
  264. key: 'by-phone',
  265. dataTestId: 'prejoin.joinByPhone',
  266. icon: IconPhoneRinging,
  267. label: t('prejoin.joinAudioByPhone'),
  268. onButtonClick: this._showDialog,
  269. onKeyPressed: this._showDialogKeyPress
  270. };
  271. return {
  272. noAudio,
  273. byPhone
  274. };
  275. }
  276. /**
  277. * Implements React's {@link Component#render()}.
  278. *
  279. * @inheritdoc
  280. * @returns {ReactElement}
  281. */
  282. render() {
  283. const {
  284. deviceStatusVisible,
  285. hasJoinByPhoneButton,
  286. joinConference,
  287. joinConferenceWithoutAudio,
  288. name,
  289. participantId,
  290. prejoinConfig,
  291. readOnlyName,
  292. showCameraPreview,
  293. showDialog,
  294. t,
  295. videoTrack
  296. } = this.props;
  297. const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress,
  298. _onOptionsClick, _setName } = this;
  299. const extraJoinButtons = this._getExtraJoinButtons();
  300. let extraButtonsToRender = Object.values(extraJoinButtons).filter((val: Object) =>
  301. !(prejoinConfig?.hideExtraJoinButtons || []).includes(val.key)
  302. );
  303. if (!hasJoinByPhoneButton) {
  304. extraButtonsToRender = extraButtonsToRender.filter((btn: Object) => btn.key !== 'by-phone');
  305. }
  306. const hasExtraJoinButtons = Boolean(extraButtonsToRender.length);
  307. const { showJoinByPhoneButtons, showError } = this.state;
  308. return (
  309. <PreMeetingScreen
  310. showDeviceStatus = { deviceStatusVisible }
  311. title = { t('prejoin.joinMeeting') }
  312. videoMuted = { !showCameraPreview }
  313. videoTrack = { videoTrack }>
  314. <div
  315. className = 'prejoin-input-area'
  316. data-testid = 'prejoin.screen'>
  317. {this.showDisplayNameField ? (<InputField
  318. autoComplete = { 'name' }
  319. autoFocus = { true }
  320. className = { showError ? 'error' : '' }
  321. hasError = { showError }
  322. onChange = { _setName }
  323. onSubmit = { joinConference }
  324. placeHolder = { t('dialog.enterDisplayName') }
  325. readOnly = { readOnlyName }
  326. value = { name } />
  327. ) : (
  328. <div className = 'prejoin-avatar-container'>
  329. <Avatar
  330. className = 'prejoin-avatar'
  331. displayName = { name }
  332. participantId = { participantId }
  333. size = { 72 } />
  334. <div className = 'prejoin-avatar-name'>{name}</div>
  335. </div>
  336. )}
  337. {showError && <div
  338. className = 'prejoin-error'
  339. data-testid = 'prejoin.errorMessage'>{t('prejoin.errorMissingName')}</div>}
  340. <div className = 'prejoin-preview-dropdown-container'>
  341. <InlineDialog
  342. content = { hasExtraJoinButtons && <div className = 'prejoin-preview-dropdown-btns'>
  343. {extraButtonsToRender.map(({ key, ...rest }: Object) => (
  344. <DropdownButton
  345. key = { key }
  346. { ...rest } />
  347. ))}
  348. </div> }
  349. isOpen = { showJoinByPhoneButtons }
  350. onClose = { _onDropdownClose }>
  351. <ActionButton
  352. OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
  353. ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
  354. ariaLabel = { t('prejoin.joinMeeting') }
  355. ariaPressed = { showJoinByPhoneButtons }
  356. hasOptions = { hasExtraJoinButtons }
  357. onClick = { _onJoinButtonClick }
  358. onKeyPress = { _onJoinKeyPress }
  359. onOptionsClick = { _onOptionsClick }
  360. role = 'button'
  361. tabIndex = { 0 }
  362. testId = 'prejoin.joinMeeting'
  363. type = 'primary'>
  364. { t('prejoin.joinMeeting') }
  365. </ActionButton>
  366. </InlineDialog>
  367. </div>
  368. </div>
  369. { showDialog && (
  370. <JoinByPhoneDialog
  371. joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
  372. onClose = { _closeDialog } />
  373. )}
  374. </PreMeetingScreen>
  375. );
  376. }
  377. }
  378. /**
  379. * Maps (parts of) the redux state to the React {@code Component} props.
  380. *
  381. * @param {Object} state - The redux state.
  382. * @returns {Object}
  383. */
  384. function mapStateToProps(state): Object {
  385. const name = getDisplayName(state);
  386. const showErrorOnJoin = isDisplayNameRequired(state) && !name;
  387. const { id: participantId } = getLocalParticipant(state);
  388. return {
  389. canEditDisplayName: isPrejoinDisplayNameVisible(state),
  390. deviceStatusVisible: isDeviceStatusVisible(state),
  391. hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
  392. name,
  393. participantId,
  394. prejoinConfig: state['features/base/config'].prejoinConfig,
  395. readOnlyName: isNameReadOnly(state),
  396. showCameraPreview: !isVideoMutedByUser(state),
  397. showDialog: isJoinByPhoneDialogVisible(state),
  398. showErrorOnJoin,
  399. videoTrack: getLocalJitsiVideoTrack(state)
  400. };
  401. }
  402. const mapDispatchToProps = {
  403. joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
  404. joinConference: joinConferenceAction,
  405. setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
  406. updateSettings
  407. };
  408. export default connect(mapStateToProps, mapDispatchToProps)(translate(Prejoin));