您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

Prejoin.web.js 14KB

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